import { colors, EtherscanLinkSuffixes, utils as sharedUtils } from '@0x/react-shared';
import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import * as _ from 'lodash';
import Divider from 'material-ui/Divider';
import { Table, TableBody, TableHeader, TableHeaderColumn, TableRow, TableRowColumn } from 'material-ui/Table';
import * as moment from 'moment';
import * as React from 'react';
import ReactTooltip from 'react-tooltip';
import { Blockchain } from 'ts/blockchain';
import { EthWethConversionButton } from 'ts/components/eth_weth_conversion_button';
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 { utils } from 'ts/utils/utils';
const DATE_FORMAT = 'D/M/YY';
const ICON_DIMENSION = 40;
const ETHER_ICON_PATH = '/images/ether.png';
const OUTDATED_WETH_ICON_PATH = '/images/wrapped_eth_gray.png';
interface EthWrappersProps {
networkId: number;
blockchain: Blockchain;
dispatcher: Dispatcher;
tokenByAddress: TokenByAddress;
userAddress: string;
userEtherBalanceInWei?: BigNumber;
lastForceTokenStateRefetch: number;
isFullWidth?: boolean;
}
interface EthWrappersState {
ethTokenState: TokenState;
outdatedWETHStateByAddress: TokenStateByAddress;
}
export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersState> {
public static defaultProps: Partial<EthWrappersProps> = {
isFullWidth: false,
};
private _isUnmounted: boolean;
constructor(props: EthWrappersProps) {
super(props);
this._isUnmounted = false;
const outdatedWETHAddresses = this._getOutdatedWETHAddresses();
const outdatedWETHStateByAddress: TokenStateByAddress = {};
_.each(outdatedWETHAddresses, outdatedWETHAddress => {
outdatedWETHStateByAddress[outdatedWETHAddress] = {
balance: new BigNumber(0),
allowance: new BigNumber(0),
isLoaded: false,
};
});
this.state = {
outdatedWETHStateByAddress,
ethTokenState: {
balance: new BigNumber(0),
allowance: new BigNumber(0),
isLoaded: false,
},
};
}
public componentWillReceiveProps(nextProps: EthWrappersProps): void {
if (
nextProps.userAddress !== this.props.userAddress ||
nextProps.networkId !== this.props.networkId ||
nextProps.lastForceTokenStateRefetch !== this.props.lastForceTokenStateRefetch
) {
// tslint:disable-next-line:no-floating-promises
this._fetchWETHStateAsync();
}
}
public componentDidMount(): void {
window.scrollTo(0, 0);
// tslint:disable-next-line:no-floating-promises
this._fetchWETHStateAsync();
}
public componentWillUnmount(): void {
this._isUnmounted = true;
}
public render(): React.ReactNode {
const etherToken = this._getEthToken();
const wethBalance = Web3Wrapper.toUnitAmount(this.state.ethTokenState.balance, constants.DECIMAL_PLACES_ETH);
const isBidirectional = true;
const etherscanUrl = sharedUtils.getEtherScanLinkIfExists(
etherToken.address,
this.props.networkId,
EtherscanLinkSuffixes.Address,
);
const tokenLabel = this._renderToken(
'Wrapped Ether',
etherToken.address,
utils.getTokenIconUrl(etherToken.symbol),
);
const userEtherBalanceInEth = !_.isUndefined(this.props.userEtherBalanceInWei)
? Web3Wrapper.toUnitAmount(this.props.userEtherBalanceInWei, constants.DECIMAL_PLACES_ETH)
: undefined;
const rootClassName = this.props.isFullWidth ? 'clearfix' : 'clearfix lg-px4 md-px4 sm-px2';
return (
<div className={rootClassName} style={{ minHeight: 600 }}>
<div className="relative">
<h3>ETH Wrapper</h3>
<div className="absolute" style={{ top: 0, right: 0 }}>
<a target="_blank" href={constants.URL_WETH_IO} style={{ color: colors.grey }}>
<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: 'transparent' }}>
<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="ml2 sm-hide xs-hide" style={{ marginTop: 12 }}>
ETH
</div>
</div>
</TableRowColumn>
<TableRowColumn>
{!_.isUndefined(userEtherBalanceInEth) ? (
`${userEtherBalanceInEth.toFixed(configs.AMOUNT_DISPLAY_PRECSION)} ETH`
) : (
<i className="zmdi zmdi-spinner zmdi-hc-spin" />
)}
</TableRowColumn>
<TableRowColumn>
<EthWethConversionButton
refetchEthTokenStateAsync={this._refetchEthTokenStateAsync.bind(this)}
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
userAddress={this.props.userAddress}
networkId={this.props.networkId}
isOutdatedWrappedEther={false}
direction={Side.Deposit}
ethToken={etherToken}
dispatcher={this.props.dispatcher}
blockchain={this.props.blockchain}
userEtherBalanceInWei={this.props.userEtherBalanceInWei}
isDisabled={_.isUndefined(userEtherBalanceInEth)}
/>
</TableRowColumn>
</TableRow>
<TableRow key="WETH">
<TableRowColumn className="py1">
{this._renderTokenLink(tokenLabel, etherscanUrl)}
</TableRowColumn>
<TableRowColumn>
{this.state.ethTokenState.isLoaded ? (
`${wethBalance.toFixed(configs.AMOUNT_DISPLAY_PRECSION)} WETH`
) : (
<i className="zmdi zmdi-spinner zmdi-hc-spin" />
)}
</TableRowColumn>
<TableRowColumn>
<EthWethConversionButton
refetchEthTokenStateAsync={this._refetchEthTokenStateAsync.bind(this)}
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
userAddress={this.props.userAddress}
networkId={this.props.networkId}
isOutdatedWrappedEther={false}
direction={Side.Receive}
isDisabled={!this.state.ethTokenState.isLoaded}
ethToken={etherToken}
dispatcher={this.props.dispatcher}
blockchain={this.props.blockchain}
userEtherBalanceInWei={this.props.userEtherBalanceInWei}
/>
</TableRowColumn>
</TableRow>
</TableBody>
</Table>
</div>
</div>
<div>
<h4>Outdated WETH</h4>
<Divider />
<div className="pt2" style={{ lineHeight: 1.5 }}>
The{' '}
<a href={constants.URL_CANONICAL_WETH_POST} 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: 'transparent' }}>
<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)}</TableBody>
</Table>
</div>
</div>
</div>
);
}
private _renderActionColumnTitle(isBidirectional: boolean): React.ReactNode {
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): React.ReactNode {
const rows = _.map(
configs.OUTDATED_WRAPPED_ETHERS,
(outdatedWETHByNetworkId: OutdatedWrappedEtherByNetworkId) => {
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);
const endMoment = moment(timestampMsRange.endTimestampMs);
dateRange = `${startMoment.format(DATE_FORMAT)}-${endMoment.format(DATE_FORMAT)}`;
} else {
dateRange = '-';
}
const outdatedEtherToken = {
...etherToken,
address: outdatedWETHIfExists.address,
};
const outdatedEtherTokenState = this.state.outdatedWETHStateByAddress[outdatedWETHIfExists.address];
const isStateLoaded = outdatedEtherTokenState.isLoaded;
const balanceInEthIfExists = isStateLoaded
? Web3Wrapper.toUnitAmount(outdatedEtherTokenState.balance, constants.DECIMAL_PLACES_ETH).toFixed(
configs.AMOUNT_DISPLAY_PRECSION,
)
: undefined;
const onConversionSuccessful = this._onOutdatedConversionSuccessfulAsync.bind(
this,
outdatedWETHIfExists.address,
);
const etherscanUrl = sharedUtils.getEtherScanLinkIfExists(
outdatedWETHIfExists.address,
this.props.networkId,
EtherscanLinkSuffixes.Address,
);
const tokenLabel = this._renderToken(dateRange, outdatedEtherToken.address, OUTDATED_WETH_ICON_PATH);
return (
<TableRow key={`weth-${outdatedWETHIfExists.address}`}>
<TableRowColumn className="py1">
{this._renderTokenLink(tokenLabel, etherscanUrl)}
</TableRowColumn>
<TableRowColumn>
{isStateLoaded ? (
`${balanceInEthIfExists} WETH`
) : (
<i className="zmdi zmdi-spinner zmdi-hc-spin" />
)}
</TableRowColumn>
<TableRowColumn>
<EthWethConversionButton
refetchEthTokenStateAsync={this._refetchEthTokenStateAsync.bind(this)}
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
userAddress={this.props.userAddress}
networkId={this.props.networkId}
isDisabled={!isStateLoaded}
isOutdatedWrappedEther={true}
direction={Side.Receive}
ethToken={outdatedEtherToken}
dispatcher={this.props.dispatcher}
blockchain={this.props.blockchain}
userEtherBalanceInWei={this.props.userEtherBalanceInWei}
onConversionSuccessful={onConversionSuccessful}
/>
</TableRowColumn>
</TableRow>
);
},
);
return rows;
}
private _renderTokenLink(tokenLabel: React.ReactNode, etherscanUrl: string): React.ReactNode {
return (
<span>
{_.isUndefined(etherscanUrl) ? (
tokenLabel
) : (
<a href={etherscanUrl} target="_blank" style={{ textDecoration: 'none' }}>
{tokenLabel}
</a>
)}
</span>
);
}
private _renderToken(name: string, address: string, imgPath: string): React.ReactNode {
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): Promise<void> {
const currentOutdatedWETHState = this.state.outdatedWETHStateByAddress[outdatedWETHAddress];
this.setState({
outdatedWETHStateByAddress: {
...this.state.outdatedWETHStateByAddress,
[outdatedWETHAddress]: {
balance: currentOutdatedWETHState.balance,
allowance: currentOutdatedWETHState.allowance,
isLoaded: false,
},
},
});
const userAddressIfExists = _.isEmpty(this.props.userAddress) ? undefined : this.props.userAddress;
const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
userAddressIfExists,
outdatedWETHAddress,
);
this.setState({
outdatedWETHStateByAddress: {
...this.state.outdatedWETHStateByAddress,
[outdatedWETHAddress]: {
balance,
allowance,
isLoaded: true,
},
},
});
}
private async _fetchWETHStateAsync(): Promise<void> {
const tokens = _.values(this.props.tokenByAddress);
const wethToken = _.find(tokens, token => token.symbol === 'WETH');
const userAddressIfExists = _.isEmpty(this.props.userAddress) ? undefined : this.props.userAddress;
const [wethBalance, wethAllowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
userAddressIfExists,
wethToken.address,
);
const outdatedWETHAddresses = this._getOutdatedWETHAddresses();
const outdatedWETHStateByAddress: TokenStateByAddress = {};
for (const address of outdatedWETHAddresses) {
const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
userAddressIfExists,
address,
);
outdatedWETHStateByAddress[address] = {
balance,
allowance,
isLoaded: true,
};
}
if (!this._isUnmounted) {
this.setState({
outdatedWETHStateByAddress,
ethTokenState: {
balance: wethBalance,
allowance: wethAllowance,
isLoaded: true,
},
});
}
}
private _getOutdatedWETHAddresses(): string[] {
const outdatedWETHAddresses = _.compact(
_.map(configs.OUTDATED_WRAPPED_ETHERS, outdatedWrappedEtherByNetwork => {
const outdatedWrappedEtherIfExists = outdatedWrappedEtherByNetwork[this.props.networkId];
if (_.isUndefined(outdatedWrappedEtherIfExists)) {
return undefined;
}
const address = outdatedWrappedEtherIfExists.address;
return address;
}),
);
return outdatedWETHAddresses;
}
private _getEthToken(): Token {
const tokens = _.values(this.props.tokenByAddress);
const etherToken = _.find(tokens, { symbol: 'WETH' });
return etherToken;
}
private async _refetchEthTokenStateAsync(): Promise<void> {
const etherToken = this._getEthToken();
const userAddressIfExists = _.isEmpty(this.props.userAddress) ? undefined : this.props.userAddress;
const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
userAddressIfExists,
etherToken.address,
);
this.setState({
ethTokenState: {
balance,
allowance,
isLoaded: true,
},
});
}
} // tslint:disable:max-file-line-count