2018-01-30 20:45:09 +01:00

443 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { ZeroEx } from '0x.js';
import { BigNumber } from '@0xproject/utils';
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 = 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,
TokenByAddress,
TokenState,
} from 'ts/types';
import { colors } from 'ts/utils/colors';
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';
const ICON_DIMENSION = 40;
const ETHER_ICON_PATH = '/images/ether.png';
const OUTDATED_WETH_ICON_PATH = '/images/wrapped_eth_gray.png';
interface OutdatedWETHAddressToIsStateLoaded {
[address: string]: boolean;
}
interface OutdatedWETHStateByAddress {
[address: string]: TokenState;
}
interface EthWrappersProps {
networkId: number;
blockchain: Blockchain;
dispatcher: Dispatcher;
tokenByAddress: TokenByAddress;
userAddress: string;
userEtherBalance: BigNumber;
lastForceTokenStateRefetch: number;
}
interface EthWrappersState {
ethTokenState: TokenState;
isWethStateLoaded: boolean;
outdatedWETHAddressToIsStateLoaded: OutdatedWETHAddressToIsStateLoaded;
outdatedWETHStateByAddress: OutdatedWETHStateByAddress;
}
export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersState> {
private _isUnmounted: boolean;
constructor(props: EthWrappersProps) {
super(props);
this._isUnmounted = false;
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,
isWethStateLoaded: false,
ethTokenState: {
balance: new BigNumber(0),
allowance: new BigNumber(0),
},
};
}
public componentWillReceiveProps(nextProps: EthWrappersProps) {
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() {
window.scrollTo(0, 0);
// tslint:disable-next-line:no-floating-promises
this._fetchWETHStateAsync();
}
public componentWillUnmount() {
this._isUnmounted = true;
}
public render() {
const etherToken = this._getEthToken();
const wethBalance = ZeroEx.toUnitAmount(this.state.ethTokenState.balance, constants.DECIMAL_PLACES_ETH);
const isBidirectional = true;
const etherscanUrl = utils.getEtherScanLinkIfExists(
etherToken.address,
this.props.networkId,
EtherscanLinkSuffixes.Address,
);
const tokenLabel = this._renderToken('Wrapped Ether', etherToken.address, configs.ICON_URL_BY_SYMBOL.WETH);
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={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: 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="ml2 sm-hide xs-hide" style={{ marginTop: 12 }}>
ETH
</div>
</div>
</TableRowColumn>
<TableRowColumn>
{this.props.userEtherBalance.toFixed(PRECISION)} ETH
</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}
userEtherBalance={this.props.userEtherBalance}
/>
</TableRowColumn>
</TableRow>
<TableRow key="WETH">
<TableRowColumn className="py1">
{this._renderTokenLink(tokenLabel, etherscanUrl)}
</TableRowColumn>
<TableRowColumn>
{this.state.isWethStateLoaded ? (
`${wethBalance.toFixed(PRECISION)} 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.isWethStateLoaded}
ethToken={etherToken}
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, this.state.ethTokenState)}
</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.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 isStateLoaded = this.state.outdatedWETHAddressToIsStateLoaded[outdatedWETHIfExists.address];
const outdatedEtherTokenState = this.state.outdatedWETHStateByAddress[outdatedWETHIfExists.address];
const balanceInEthIfExists = isStateLoaded
? ZeroEx.toUnitAmount(outdatedEtherTokenState.balance, constants.DECIMAL_PLACES_ETH).toFixed(
PRECISION,
)
: undefined;
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-${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}
ethTokenState={outdatedEtherTokenState}
dispatcher={this.props.dispatcher}
blockchain={this.props.blockchain}
userEtherBalance={this.props.userEtherBalance}
onConversionSuccessful={onConversionSuccessful}
/>
</TableRowColumn>
</TableRow>
);
},
);
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: {
...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 _fetchWETHStateAsync() {
const tokens = _.values(this.props.tokenByAddress);
const wethToken = _.find(tokens, token => token.symbol === 'WETH');
const [wethBalance, wethAllowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
this.props.userAddress,
wethToken.address,
);
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;
}
if (!this._isUnmounted) {
this.setState({
outdatedWETHStateByAddress,
outdatedWETHAddressToIsStateLoaded,
ethTokenState: {
balance: wethBalance,
allowance: wethAllowance,
},
isWethStateLoaded: 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() {
const tokens = _.values(this.props.tokenByAddress);
const etherToken = _.find(tokens, { symbol: 'WETH' });
return etherToken;
}
private async _refetchEthTokenStateAsync() {
const etherToken = this._getEthToken();
const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
this.props.userAddress,
etherToken.address,
);
this.setState({
ethTokenState: {
balance,
allowance,
},
});
}
} // tslint:disable:max-file-line-count