diff options
-rw-r--r-- | packages/website/ts/blockchain.ts | 17 | ||||
-rw-r--r-- | packages/website/ts/components/dialogs/eth_weth_conversion_dialog.tsx | 16 | ||||
-rw-r--r-- | packages/website/ts/components/dialogs/wrapped_eth_section_notice_dialog.tsx | 38 | ||||
-rw-r--r-- | packages/website/ts/components/eth_wrappers.tsx | 105 | ||||
-rw-r--r-- | packages/website/ts/components/portal.tsx | 50 | ||||
-rw-r--r-- | packages/website/ts/components/send_button.tsx | 2 | ||||
-rw-r--r-- | packages/website/ts/utils/configs.ts | 7 | ||||
-rw-r--r-- | packages/website/ts/utils/constants.ts | 2 |
8 files changed, 188 insertions, 49 deletions
diff --git a/packages/website/ts/blockchain.ts b/packages/website/ts/blockchain.ts index a42b19cff..7a0d546b4 100644 --- a/packages/website/ts/blockchain.ts +++ b/packages/website/ts/blockchain.ts @@ -158,6 +158,12 @@ export class Blockchain { } public async isAddressInTokenRegistryAsync(tokenAddress: string): Promise<boolean> { utils.assert(!_.isUndefined(this.zeroEx), 'ZeroEx must be instantiated.'); + // HACK: temporarily whitelist the new WETH token address `as if` they were + // already in the tokenRegistry. + if (configs.shouldDeprecateOldWethToken && + tokenAddress === configs.newWrappedEthers[this.networkId]) { + return true; + } const tokenIfExists = await this.zeroEx.tokenRegistry.getTokenIfExistsAsync(tokenAddress); return !_.isUndefined(tokenIfExists); } @@ -598,12 +604,11 @@ export class Blockchain { // new canonical WETH. // TODO: Remove this hack once we've updated the TokenRegistries let address = t.address; - if (t.symbol === 'WETH') { - if (this.networkId === 1) { - address = '0xe495bcacaf29a0eb00fb67b86e9cd2a994dd55d8'; - } else if (this.networkId === 42) { - address = '0x739e78d6bebbdf24105a5145fa04436589d1cbd9'; - } + if (configs.shouldDeprecateOldWethToken && t.symbol === 'WETH') { + const newEtherTokenAddressIfExists = configs.newWrappedEthers[this.networkId]; + if (!_.isUndefined(newEtherTokenAddressIfExists)) { + address = newEtherTokenAddressIfExists; + } } const token: Token = { iconUrl, 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 c8bdced9b..230ac5183 100644 --- a/packages/website/ts/components/dialogs/eth_weth_conversion_dialog.tsx +++ b/packages/website/ts/components/dialogs/eth_weth_conversion_dialog.tsx @@ -109,7 +109,16 @@ export class EthWethConversionDialog extends className="pt1" style={{fontSize: 12}} > - 1 ETH = 1 WETH + <div className="left">1 ETH = 1 WETH</div> + {this.props.direction === Side.receive && + <div + className="right" + onClick={this.onMaxClick.bind(this)} + style={{color: DARK_BLUE, textDecoration: 'underline', cursor: 'pointer'}} + > + Max + </div> + } </div> </div> </div> @@ -137,6 +146,11 @@ export class EthWethConversionDialog extends </div> ); } + private onMaxClick() { + this.setState({ + value: this.props.tokenState.balance, + }); + } private onValueChange(isValid: boolean, amount?: BigNumber) { this.setState({ value: amount, diff --git a/packages/website/ts/components/dialogs/wrapped_eth_section_notice_dialog.tsx b/packages/website/ts/components/dialogs/wrapped_eth_section_notice_dialog.tsx new file mode 100644 index 000000000..d13b2e8ce --- /dev/null +++ b/packages/website/ts/components/dialogs/wrapped_eth_section_notice_dialog.tsx @@ -0,0 +1,38 @@ +import Dialog from 'material-ui/Dialog'; +import FlatButton from 'material-ui/FlatButton'; +import {colors} from 'material-ui/styles'; +import * as React from 'react'; + +interface WrappedEthSectionNoticeDialogProps { + isOpen: boolean; + onToggleDialog: () => void; +} + +export function WrappedEthSectionNoticeDialog(props: WrappedEthSectionNoticeDialogProps) { + return ( + <Dialog + title="Dedicated Wrapped Ether Section" + titleStyle={{fontWeight: 100}} + actions={[ + <FlatButton + key="acknowledgeWrapEthSection" + label="Sounds good" + onTouchTap={props.onToggleDialog.bind(this)} + />, + ]} + open={props.isOpen} + onRequestClose={props.onToggleDialog.bind(this)} + autoScrollBodyContent={true} + modal={true} + > + <div className="pt2" style={{color: colors.grey700}}> + <div> + We have recently updated the Wrapped Ether token used by 0x Portal. + Don't worry, unwrapping Ether tied to the old Wrapped Ether token can + be done at any time by clicking on the "Wrap ETH" section in the menu + to the left. + </div> + </div> + </Dialog> + ); +} diff --git a/packages/website/ts/components/eth_wrappers.tsx b/packages/website/ts/components/eth_wrappers.tsx index 58b73b11c..a10313597 100644 --- a/packages/website/ts/components/eth_wrappers.tsx +++ b/packages/website/ts/components/eth_wrappers.tsx @@ -13,10 +13,12 @@ import { } from 'material-ui/Table'; import * as moment from 'moment'; import * as React from 'react'; +import ReactTooltip = require('react-tooltip'); import {Blockchain} from 'ts/blockchain'; import {EthWethConversionButton} from 'ts/components/eth_weth_conversion_button'; import {Dispatcher} from 'ts/redux/dispatcher'; import { + EtherscanLinkSuffixes, OutdatedWrappedEtherByNetworkId, Side, Token, @@ -26,6 +28,7 @@ import { } from 'ts/types'; import {configs} from 'ts/utils/configs'; import {constants} from 'ts/utils/constants'; +import {utils} from 'ts/utils/utils'; const PRECISION = 5; const DATE_FORMAT = 'D/M/YY'; @@ -85,6 +88,10 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt const etherTokenState = this.props.tokenStateByAddress[etherToken.address]; const wethBalance = ZeroEx.toUnitAmount(etherTokenState.balance, 18); const isBidirectional = true; + const etherscanUrl = utils.getEtherScanLinkIfExists( + etherToken.address, this.props.networkId, EtherscanLinkSuffixes.address, + ); + const tokenLabel = this.renderToken('Wrapped Ether', etherToken.address, constants.iconUrlBySymbol.WETH); return ( <div className="clearfix lg-px4 md-px4 sm-px2" style={{minHeight: 600}}> <div className="relative"> @@ -92,7 +99,7 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt <div className="absolute" style={{top: 0, right: 0}}> <a target="_blank" - href="https://weth.io/" + href={constants.WETH_IO_URL} style={{color: LIGHT_GRAY}} > <div className="flex"> @@ -131,8 +138,11 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt style={{width: ICON_DIMENSION, height: ICON_DIMENSION}} src={ETHER_ICON_PATH} /> - <div className="mt2 ml2 sm-hide xs-hide"> - Ether + <div + className="ml2 sm-hide xs-hide" + style={{marginTop: 12}} + > + ETH </div> </div> </TableRowColumn> @@ -153,15 +163,7 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt </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> + {this.renderTokenLink(tokenLabel, etherscanUrl)} </TableRowColumn> <TableRowColumn> {wethBalance.toFixed(PRECISION)} WETH @@ -244,8 +246,11 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt private renderOutdatedWeths(etherToken: Token, etherTokenState: TokenState) { const rows = _.map(configs.outdatedWrappedEthers, (outdatedWETHByNetworkId: OutdatedWrappedEtherByNetworkId) => { - const outdatedWETH = outdatedWETHByNetworkId[this.props.networkId]; - const timestampMsRange = outdatedWETH.timestampMsRange; + const outdatedWETHIfExists = outdatedWETHByNetworkId[this.props.networkId]; + if (_.isUndefined(outdatedWETHIfExists)) { + return null; // noop + } + const timestampMsRange = outdatedWETHIfExists.timestampMsRange; let dateRange: string; if (!_.isUndefined(timestampMsRange)) { const startMoment = moment(timestampMsRange.startTimestampMs); @@ -256,26 +261,24 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt } const outdatedEtherToken = { ...etherToken, - address: outdatedWETH.address, + address: outdatedWETHIfExists.address, }; - const isStateLoaded = this.state.outdatedWETHAddressToIsStateLoaded[outdatedWETH.address]; - const outdatedEtherTokenState = this.state.outdatedWETHStateByAddress[outdatedWETH.address]; + const isStateLoaded = this.state.outdatedWETHAddressToIsStateLoaded[outdatedWETHIfExists.address]; + const outdatedEtherTokenState = this.state.outdatedWETHStateByAddress[outdatedWETHIfExists.address]; const balanceInEthIfExists = isStateLoaded ? ZeroEx.toUnitAmount(outdatedEtherTokenState.balance, 18).toFixed(PRECISION) : undefined; - const onConversionSuccessful = this.onOutdatedConversionSuccessfulAsync.bind(this, outdatedWETH.address); + const onConversionSuccessful = this.onOutdatedConversionSuccessfulAsync.bind( + this, outdatedWETHIfExists.address, + ); + const etherscanUrl = utils.getEtherScanLinkIfExists( + outdatedWETHIfExists.address, this.props.networkId, EtherscanLinkSuffixes.address, + ); + const tokenLabel = this.renderToken(dateRange, outdatedEtherToken.address, OUTDATED_WETH_ICON_PATH); return ( - <TableRow key={`weth-${outdatedWETH.address}`}> + <TableRow key={`weth-${outdatedWETHIfExists.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> + {this.renderTokenLink(tokenLabel, etherscanUrl)} </TableRowColumn> <TableRowColumn> {isStateLoaded ? @@ -301,6 +304,41 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt }); return rows; } + private renderTokenLink(tokenLabel: React.ReactNode, etherscanUrl: string) { + return ( + <span> + {_.isUndefined(etherscanUrl) ? + tokenLabel : + <a href={etherscanUrl} target="_blank" style={{textDecoration: 'none'}}> + {tokenLabel} + </a> + } + </span> + ); + } + private renderToken(name: string, address: string, imgPath: string) { + const tooltipId = `tooltip-${address}`; + return ( + <div className="flex"> + <img + style={{width: ICON_DIMENSION, height: ICON_DIMENSION}} + src={imgPath} + /> + <div + className="ml2 sm-hide xs-hide" + style={{marginTop: 12}} + > + <span + data-tip={true} + data-for={tooltipId} + > + {name} + </span> + <ReactTooltip id={tooltipId}>{address}</ReactTooltip> + </div> + </div> + ); + } private async onOutdatedConversionSuccessfulAsync(outdatedWETHAddress: string) { this.setState({ outdatedWETHAddressToIsStateLoaded: { @@ -345,9 +383,14 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt }); } private getOutdatedWETHAddresses(): string[] { - const outdatedWETHAddresses = _.map(configs.outdatedWrappedEthers, outdatedWrappedEther => { - return outdatedWrappedEther[this.props.networkId].address; - }); + const outdatedWETHAddresses = _.compact(_.map(configs.outdatedWrappedEthers, outdatedWrappedEtherByNetwork => { + const outdatedWrappedEtherIfExists = outdatedWrappedEtherByNetwork[this.props.networkId]; + if (_.isUndefined(outdatedWrappedEtherIfExists)) { + return undefined; + } + const address = outdatedWrappedEtherIfExists.address; + return address; + })); return outdatedWETHAddresses; } } // tslint:disable:max-file-line-count diff --git a/packages/website/ts/components/portal.tsx b/packages/website/ts/components/portal.tsx index 74b1bdec6..69a7c1e7f 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 {WrappedEthSectionNoticeDialog} from 'ts/components/dialogs/wrapped_eth_section_notice_dialog'; import {EthWrappers} from 'ts/components/eth_wrappers'; import {FillOrder} from 'ts/components/fill_order'; import {Footer} from 'ts/components/footer'; @@ -63,22 +64,39 @@ interface PortalAllState { prevNetworkId: number; prevNodeVersion: string; prevUserAddress: string; - hasAcceptedDisclaimer: boolean; + prevPathname: string; + isDisclaimerDialogOpen: boolean; + isWethNoticeDialogOpen: boolean; } export class Portal extends React.Component<PortalAllProps, PortalAllState> { private blockchain: Blockchain; private sharedOrderIfExists: Order; private throttledScreenWidthUpdate: () => void; + public static hasAlreadyDismissedWethNotice() { + const didDismissWethNotice = localStorage.getItemIfExists(constants.DISMISS_WETH_NOTICE_LOCAL_STORAGE_KEY); + const hasAlreadyDismissedWethNotice = !_.isUndefined(didDismissWethNotice) && + !_.isEmpty(didDismissWethNotice); + return hasAlreadyDismissedWethNotice; + } constructor(props: PortalAllProps) { super(props); this.sharedOrderIfExists = this.getSharedOrderIfExists(); this.throttledScreenWidthUpdate = _.throttle(this.updateScreenWidth.bind(this), THROTTLE_TIMEOUT); + + const isViewingBalances = _.includes(props.location.pathname, `${WebsitePaths.Portal}/balances`); + const hasAlreadyDismissedWethNotice = Portal.hasAlreadyDismissedWethNotice(); + + const didAcceptPortalDisclaimer = localStorage.getItemIfExists(constants.ACCEPT_DISCLAIMER_LOCAL_STORAGE_KEY); + const hasAcceptedDisclaimer = !_.isUndefined(didAcceptPortalDisclaimer) && + !_.isEmpty(didAcceptPortalDisclaimer); this.state = { prevNetworkId: this.props.networkId, prevNodeVersion: this.props.nodeVersion, prevUserAddress: this.props.userAddress, - hasAcceptedDisclaimer: false, + prevPathname: this.props.location.pathname, + isDisclaimerDialogOpen: !hasAcceptedDisclaimer, + isWethNoticeDialogOpen: !hasAlreadyDismissedWethNotice && isViewingBalances, }; } public componentDidMount() { @@ -87,12 +105,6 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> { } public componentWillMount() { this.blockchain = new Blockchain(this.props.dispatcher); - const didAcceptPortalDisclaimer = localStorage.getItemIfExists(constants.ACCEPT_DISCLAIMER_LOCAL_STORAGE_KEY); - const hasAcceptedDisclaimer = !_.isUndefined(didAcceptPortalDisclaimer) && - !_.isEmpty(didAcceptPortalDisclaimer); - this.setState({ - hasAcceptedDisclaimer, - }); } public componentWillUnmount() { this.blockchain.destroy(); @@ -128,6 +140,14 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> { // tslint:disable-next-line:no-floating-promises this.blockchain.nodeVersionUpdatedFireAndForgetAsync(nextProps.nodeVersion); } + if (nextProps.location.pathname !== this.state.prevPathname) { + const isViewingBalances = _.includes(nextProps.location.pathname, `${WebsitePaths.Portal}/balances`); + const hasAlreadyDismissedWethNotice = Portal.hasAlreadyDismissedWethNotice(); + this.setState({ + prevPathname: nextProps.location.pathname, + isWethNoticeDialogOpen: !hasAlreadyDismissedWethNotice && isViewingBalances, + }); + } } public render() { const updateShouldBlockchainErrDialogBeOpen = this.props.dispatcher @@ -215,8 +235,12 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> { toggleDialogFn={updateShouldBlockchainErrDialogBeOpen} networkId={this.props.networkId} /> + <WrappedEthSectionNoticeDialog + isOpen={this.state.isWethNoticeDialogOpen} + onToggleDialog={this.onWethNoticeAccepted.bind(this)} + /> <PortalDisclaimerDialog - isOpen={!this.state.hasAcceptedDisclaimer} + isOpen={this.state.isDisclaimerDialogOpen} onToggleDialog={this.onPortalDisclaimerAccepted.bind(this)} /> <FlashMessage @@ -297,7 +321,13 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> { private onPortalDisclaimerAccepted() { localStorage.setItem(constants.ACCEPT_DISCLAIMER_LOCAL_STORAGE_KEY, 'set'); this.setState({ - hasAcceptedDisclaimer: true, + isDisclaimerDialogOpen: false, + }); + } + private onWethNoticeAccepted() { + localStorage.setItem(constants.DISMISS_WETH_NOTICE_LOCAL_STORAGE_KEY, 'set'); + this.setState({ + isWethNoticeDialogOpen: false, }); } private getSharedOrderIfExists(): Order { diff --git a/packages/website/ts/components/send_button.tsx b/packages/website/ts/components/send_button.tsx index b3fd2aeba..d8d3c7f56 100644 --- a/packages/website/ts/components/send_button.tsx +++ b/packages/website/ts/components/send_button.tsx @@ -76,8 +76,8 @@ export class SendButton extends React.Component<SendButtonProps, SendButtonState } else if (!_.includes(errMsg, 'User denied transaction')) { utils.consoleLog(`Unexpected error encountered: ${err}`); utils.consoleLog(err.stack); - await errorReporter.reportAsync(err); this.props.onError(); + await errorReporter.reportAsync(err); } } this.setState({ diff --git a/packages/website/ts/utils/configs.ts b/packages/website/ts/utils/configs.ts index 4a08929cf..20efc66be 100644 --- a/packages/website/ts/utils/configs.ts +++ b/packages/website/ts/utils/configs.ts @@ -19,6 +19,13 @@ export const configs = { lastLocalStorageFillClearanceDate: '2017-11-22', lastLocalStorageTrackedTokenClearanceDate: '2017-12-13', isMainnetEnabled: true, + shouldDeprecateOldWethToken: true, + // newWrappedEthers is temporary until we remove the shouldDeprecateOldWethToken flag + // and add the new WETHs to the tokenRegistry + newWrappedEthers: { + 1: '0xe495bcacaf29a0eb00fb67b86e9cd2a994dd55d8', + 42: '0x739e78d6bebbdf24105a5145fa04436589d1cbd9', + } as {[networkId: string]: string}, outdatedWrappedEthers: [ { 42: { diff --git a/packages/website/ts/utils/constants.ts b/packages/website/ts/utils/constants.ts index fb81dd9a9..cb2dbef88 100644 --- a/packages/website/ts/utils/constants.ts +++ b/packages/website/ts/utils/constants.ts @@ -34,6 +34,7 @@ export const constants = { GITHUB_URL: 'https://github.com/0xProject', GITHUB_WIKI_URL: 'https://github.com/0xProject/wiki', HTTP_NO_CONTENT_STATUS_CODE: 204, + DISMISS_WETH_NOTICE_LOCAL_STORAGE_KEY: 'hasDismissedWethNotice', ACCEPT_DISCLAIMER_LOCAL_STORAGE_KEY: 'didAcceptPortalDisclaimer', LINKEDIN_0X_URL: 'https://www.linkedin.com/company/0x', LEDGER_PROVIDER_NAME: 'Ledger', @@ -83,6 +84,7 @@ export const constants = { WEB3_DECODED_LOG_ENTRY_EVENT_URL: 'https://github.com/0xProject/web3-typescript-typings/blob/f5bcb96/index.d.ts#L123', WEB3_LOG_ENTRY_EVENT_URL: 'https://github.com/0xProject/web3-typescript-typings/blob/f5bcb96/index.d.ts#L127', + WETH_IO_URL: 'https://weth.io/', ZEROEX_CHAT_URL: 'https://chat.0xproject.com', // Projects ETHFINEX_URL: 'https://www.bitfinex.com/ethfinex', |