aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBrandon Millman <brandon@0xproject.com>2018-03-22 06:48:13 +0800
committerGitHub <noreply@github.com>2018-03-22 06:48:13 +0800
commit8478dc8d6d05efcdeac6653872f35149f3c9589c (patch)
tree372cb687314dc0c550450f67b5dde7ffe21464f2
parent8d01956c3ec7e580284091c91f7be2c0b0081695 (diff)
parent9f8e41cbfacf98f381bb388180f1134c01501321 (diff)
downloaddexon-0x-contracts-8478dc8d6d05efcdeac6653872f35149f3c9589c.tar
dexon-0x-contracts-8478dc8d6d05efcdeac6653872f35149f3c9589c.tar.gz
dexon-0x-contracts-8478dc8d6d05efcdeac6653872f35149f3c9589c.tar.bz2
dexon-0x-contracts-8478dc8d6d05efcdeac6653872f35149f3c9589c.tar.lz
dexon-0x-contracts-8478dc8d6d05efcdeac6653872f35149f3c9589c.tar.xz
dexon-0x-contracts-8478dc8d6d05efcdeac6653872f35149f3c9589c.tar.zst
dexon-0x-contracts-8478dc8d6d05efcdeac6653872f35149f3c9589c.zip
Merge pull request #468 from 0xProject/feature/website/wallet-interface
Implement initial wallet interface
-rw-r--r--packages/react-shared/CHANGELOG.md4
-rw-r--r--packages/react-shared/src/utils/colors.ts3
-rw-r--r--packages/website/ts/components/eth_wrappers.tsx59
-rw-r--r--packages/website/ts/components/portal.tsx40
-rw-r--r--packages/website/ts/components/portal_menu.tsx13
-rw-r--r--packages/website/ts/components/token_balances.tsx9
-rw-r--r--packages/website/ts/components/wallet.tsx373
-rw-r--r--packages/website/ts/types.ts15
8 files changed, 470 insertions, 46 deletions
diff --git a/packages/react-shared/CHANGELOG.md b/packages/react-shared/CHANGELOG.md
index 43b92d58a..bf0fb7ab1 100644
--- a/packages/react-shared/CHANGELOG.md
+++ b/packages/react-shared/CHANGELOG.md
@@ -1,3 +1,7 @@
# CHANGELOG
+## v0.1.0 - _TBD, 2018_
+
+ * Added new colors (#468)
+
## v0.0.1 - _March 8, 2018_
diff --git a/packages/react-shared/src/utils/colors.ts b/packages/react-shared/src/utils/colors.ts
index 2eead95c7..ea0165305 100644
--- a/packages/react-shared/src/utils/colors.ts
+++ b/packages/react-shared/src/utils/colors.ts
@@ -45,4 +45,7 @@ export const colors = {
orange: '#E69D00',
amber800: '#FF8F00',
darkYellow: '#caca03',
+ walletBoxShadow: 'rgba(56, 59, 137, 0.2)',
+ walletBorder: '#f5f5f6',
+ walletDefaultItemBackground: '#fbfbfc',
};
diff --git a/packages/website/ts/components/eth_wrappers.tsx b/packages/website/ts/components/eth_wrappers.tsx
index b12c637e5..59afeb50b 100644
--- a/packages/website/ts/components/eth_wrappers.tsx
+++ b/packages/website/ts/components/eth_wrappers.tsx
@@ -10,7 +10,14 @@ 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 { OutdatedWrappedEtherByNetworkId, Side, Token, TokenByAddress, TokenState } from 'ts/types';
+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';
@@ -20,13 +27,6 @@ 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;
@@ -39,9 +39,7 @@ interface EthWrappersProps {
interface EthWrappersState {
ethTokenState: TokenState;
- isWethStateLoaded: boolean;
- outdatedWETHAddressToIsStateLoaded: OutdatedWETHAddressToIsStateLoaded;
- outdatedWETHStateByAddress: OutdatedWETHStateByAddress;
+ outdatedWETHStateByAddress: TokenStateByAddress;
}
export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersState> {
@@ -50,22 +48,20 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt
super(props);
this._isUnmounted = false;
const outdatedWETHAddresses = this._getOutdatedWETHAddresses();
- const outdatedWETHAddressToIsStateLoaded: OutdatedWETHAddressToIsStateLoaded = {};
- const outdatedWETHStateByAddress: OutdatedWETHStateByAddress = {};
+ const outdatedWETHStateByAddress: TokenStateByAddress = {};
_.each(outdatedWETHAddresses, outdatedWETHAddress => {
- outdatedWETHAddressToIsStateLoaded[outdatedWETHAddress] = false;
outdatedWETHStateByAddress[outdatedWETHAddress] = {
balance: new BigNumber(0),
allowance: new BigNumber(0),
+ isLoaded: false,
};
});
this.state = {
- outdatedWETHAddressToIsStateLoaded,
outdatedWETHStateByAddress,
- isWethStateLoaded: false,
ethTokenState: {
balance: new BigNumber(0),
allowance: new BigNumber(0),
+ isLoaded: false,
},
};
}
@@ -169,7 +165,7 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt
{this._renderTokenLink(tokenLabel, etherscanUrl)}
</TableRowColumn>
<TableRowColumn>
- {this.state.isWethStateLoaded ? (
+ {this.state.ethTokenState.isLoaded ? (
`${wethBalance.toFixed(configs.AMOUNT_DISPLAY_PRECSION)} WETH`
) : (
<i className="zmdi zmdi-spinner zmdi-hc-spin" />
@@ -183,7 +179,7 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt
networkId={this.props.networkId}
isOutdatedWrappedEther={false}
direction={Side.Receive}
- isDisabled={!this.state.isWethStateLoaded}
+ isDisabled={!this.state.ethTokenState.isLoaded}
ethToken={etherToken}
dispatcher={this.props.dispatcher}
blockchain={this.props.blockchain}
@@ -266,8 +262,8 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt
...etherToken,
address: outdatedWETHIfExists.address,
};
- const isStateLoaded = this.state.outdatedWETHAddressToIsStateLoaded[outdatedWETHIfExists.address];
const outdatedEtherTokenState = this.state.outdatedWETHStateByAddress[outdatedWETHIfExists.address];
+ const isStateLoaded = outdatedEtherTokenState.isLoaded;
const balanceInEthIfExists = isStateLoaded
? ZeroEx.toUnitAmount(outdatedEtherTokenState.balance, constants.DECIMAL_PLACES_ETH).toFixed(
configs.AMOUNT_DISPLAY_PRECSION,
@@ -345,10 +341,15 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt
);
}
private async _onOutdatedConversionSuccessfulAsync(outdatedWETHAddress: string) {
+ const currentOutdatedWETHState = this.state.outdatedWETHStateByAddress[outdatedWETHAddress];
this.setState({
- outdatedWETHAddressToIsStateLoaded: {
- ...this.state.outdatedWETHAddressToIsStateLoaded,
- [outdatedWETHAddress]: false,
+ outdatedWETHStateByAddress: {
+ ...this.state.outdatedWETHStateByAddress,
+ [outdatedWETHAddress]: {
+ balance: currentOutdatedWETHState.balance,
+ allowance: currentOutdatedWETHState.allowance,
+ isLoaded: false,
+ },
},
});
const userAddressIfExists = _.isEmpty(this.props.userAddress) ? undefined : this.props.userAddress;
@@ -357,15 +358,12 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt
outdatedWETHAddress,
);
this.setState({
- outdatedWETHAddressToIsStateLoaded: {
- ...this.state.outdatedWETHAddressToIsStateLoaded,
- [outdatedWETHAddress]: true,
- },
outdatedWETHStateByAddress: {
...this.state.outdatedWETHStateByAddress,
[outdatedWETHAddress]: {
balance,
allowance,
+ isLoaded: true,
},
},
});
@@ -380,8 +378,7 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt
);
const outdatedWETHAddresses = this._getOutdatedWETHAddresses();
- const outdatedWETHAddressToIsStateLoaded: OutdatedWETHAddressToIsStateLoaded = {};
- const outdatedWETHStateByAddress: OutdatedWETHStateByAddress = {};
+ const outdatedWETHStateByAddress: TokenStateByAddress = {};
for (const address of outdatedWETHAddresses) {
const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
userAddressIfExists,
@@ -390,18 +387,17 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt
outdatedWETHStateByAddress[address] = {
balance,
allowance,
+ isLoaded: true,
};
- outdatedWETHAddressToIsStateLoaded[address] = true;
}
if (!this._isUnmounted) {
this.setState({
outdatedWETHStateByAddress,
- outdatedWETHAddressToIsStateLoaded,
ethTokenState: {
balance: wethBalance,
allowance: wethAllowance,
+ isLoaded: true,
},
- isWethStateLoaded: true,
});
}
}
@@ -434,6 +430,7 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt
ethTokenState: {
balance,
allowance,
+ isLoaded: true,
},
});
}
diff --git a/packages/website/ts/components/portal.tsx b/packages/website/ts/components/portal.tsx
index 5bdb5bde9..59eaca67e 100644
--- a/packages/website/ts/components/portal.tsx
+++ b/packages/website/ts/components/portal.tsx
@@ -19,12 +19,22 @@ import { TokenBalances } from 'ts/components/token_balances';
import { TopBar } from 'ts/components/top_bar/top_bar';
import { TradeHistory } from 'ts/components/trade_history/trade_history';
import { FlashMessage } from 'ts/components/ui/flash_message';
+import { Wallet } from 'ts/components/wallet';
import { GenerateOrderForm } from 'ts/containers/generate_order_form';
import { localStorage } from 'ts/local_storage/local_storage';
import { Dispatcher } from 'ts/redux/dispatcher';
import { portalOrderSchema } from 'ts/schemas/portal_order_schema';
import { validator } from 'ts/schemas/validator';
-import { BlockchainErrs, HashData, Order, ProviderType, ScreenWidths, TokenByAddress, WebsitePaths } from 'ts/types';
+import {
+ BlockchainErrs,
+ Environments,
+ HashData,
+ Order,
+ ProviderType,
+ ScreenWidths,
+ TokenByAddress,
+ WebsitePaths,
+} from 'ts/types';
import { configs } from 'ts/utils/configs';
import { constants } from 'ts/utils/constants';
import { Translate } from 'ts/utils/translate';
@@ -194,6 +204,12 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> {
<div className="py2" style={{ backgroundColor: colors.grey50 }}>
{this.props.blockchainIsLoaded ? (
<Switch>
+ {configs.ENVIRONMENT === Environments.DEVELOPMENT && (
+ <Route
+ path={`${WebsitePaths.Portal}/wallet`}
+ render={this._renderWallet.bind(this)}
+ />
+ )}
<Route
path={`${WebsitePaths.Portal}/weth`}
render={this._renderEthWrapper.bind(this)}
@@ -272,6 +288,28 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> {
isLedgerDialogOpen: !this.state.isLedgerDialogOpen,
});
}
+ private _renderWallet() {
+ const allTokens = _.values(this.props.tokenByAddress);
+ const trackedTokens = _.filter(allTokens, t => t.isTracked);
+ return (
+ <div className="flex flex-center">
+ <div className="mx-auto">
+ <Wallet
+ userAddress={this.props.userAddress}
+ networkId={this.props.networkId}
+ blockchain={this._blockchain}
+ blockchainIsLoaded={this.props.blockchainIsLoaded}
+ blockchainErr={this.props.blockchainErr}
+ dispatcher={this.props.dispatcher}
+ tokenByAddress={this.props.tokenByAddress}
+ trackedTokens={trackedTokens}
+ userEtherBalanceInWei={this.props.userEtherBalanceInWei}
+ lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
+ />
+ </div>
+ </div>
+ );
+ }
private _renderEthWrapper() {
return (
<EthWrappers
diff --git a/packages/website/ts/components/portal_menu.tsx b/packages/website/ts/components/portal_menu.tsx
index a2f9340c8..9ab611556 100644
--- a/packages/website/ts/components/portal_menu.tsx
+++ b/packages/website/ts/components/portal_menu.tsx
@@ -1,7 +1,8 @@
import * as _ from 'lodash';
import * as React from 'react';
import { MenuItem } from 'ts/components/ui/menu_item';
-import { WebsitePaths } from 'ts/types';
+import { Environments, WebsitePaths } from 'ts/types';
+import { configs } from 'ts/utils/configs';
export interface PortalMenuProps {
menuItemStyle: React.CSSProperties;
@@ -57,6 +58,16 @@ export class PortalMenu extends React.Component<PortalMenuProps, PortalMenuState
>
{this._renderMenuItemWithIcon('Wrap ETH', 'zmdi-circle-o')}
</MenuItem>
+ {configs.ENVIRONMENT === Environments.DEVELOPMENT && (
+ <MenuItem
+ style={this.props.menuItemStyle}
+ className="py2"
+ to={`${WebsitePaths.Portal}/wallet`}
+ onClick={this.props.onClick.bind(this)}
+ >
+ {this._renderMenuItemWithIcon('Wallet', 'zmdi-balance-wallet')}
+ </MenuItem>
+ )}
</div>
);
}
diff --git a/packages/website/ts/components/token_balances.tsx b/packages/website/ts/components/token_balances.tsx
index 186393c4f..b4a710ef4 100644
--- a/packages/website/ts/components/token_balances.tsx
+++ b/packages/website/ts/components/token_balances.tsx
@@ -37,6 +37,7 @@ import {
ScreenWidths,
Token,
TokenByAddress,
+ TokenStateByAddress,
TokenVisibility,
} from 'ts/types';
import { configs } from 'ts/utils/configs';
@@ -61,14 +62,6 @@ const styles: Styles = {
},
};
-interface TokenStateByAddress {
- [address: string]: {
- balance: BigNumber;
- allowance: BigNumber;
- isLoaded: boolean;
- };
-}
-
interface TokenBalancesProps {
blockchain: Blockchain;
blockchainErr: BlockchainErrs;
diff --git a/packages/website/ts/components/wallet.tsx b/packages/website/ts/components/wallet.tsx
new file mode 100644
index 000000000..8c6ef9cad
--- /dev/null
+++ b/packages/website/ts/components/wallet.tsx
@@ -0,0 +1,373 @@
+import { ZeroEx } from '0x.js';
+import {
+ colors,
+ constants as sharedConstants,
+ EtherscanLinkSuffixes,
+ Styles,
+ utils as sharedUtils,
+} from '@0xproject/react-shared';
+import { BigNumber } from '@0xproject/utils';
+import * as _ from 'lodash';
+import FlatButton from 'material-ui/FlatButton';
+import { List, ListItem } from 'material-ui/List';
+import NavigationArrowDownward from 'material-ui/svg-icons/navigation/arrow-downward';
+import NavigationArrowUpward from 'material-ui/svg-icons/navigation/arrow-upward';
+import * as React from 'react';
+import ReactTooltip = require('react-tooltip');
+import firstBy = require('thenby');
+
+import { Blockchain } from 'ts/blockchain';
+import { AllowanceToggle } from 'ts/components/inputs/allowance_toggle';
+import { Identicon } from 'ts/components/ui/identicon';
+import { TokenIcon } from 'ts/components/ui/token_icon';
+import { Dispatcher } from 'ts/redux/dispatcher';
+import { BalanceErrs, BlockchainErrs, Token, TokenByAddress, TokenState, TokenStateByAddress } from 'ts/types';
+import { constants } from 'ts/utils/constants';
+import { utils } from 'ts/utils/utils';
+
+export interface WalletProps {
+ userAddress?: string;
+ networkId?: number;
+ blockchain: Blockchain;
+ blockchainIsLoaded: boolean;
+ blockchainErr: BlockchainErrs;
+ dispatcher: Dispatcher;
+ tokenByAddress: TokenByAddress;
+ trackedTokens: Token[];
+ userEtherBalanceInWei: BigNumber;
+ lastForceTokenStateRefetch: number;
+}
+
+interface WalletState {
+ trackedTokenStateByAddress: TokenStateByAddress;
+}
+
+enum WrappedEtherAction {
+ Wrap,
+ Unwrap,
+}
+
+interface AllowanceToggleConfig {
+ token: Token;
+ tokenState: TokenState;
+}
+
+interface AccessoryItemConfig {
+ wrappedEtherAction?: WrappedEtherAction;
+ allowanceToggleConfig?: AllowanceToggleConfig;
+}
+
+const styles: Styles = {
+ wallet: {
+ width: 346,
+ backgroundColor: colors.white,
+ borderBottomRightRadius: 10,
+ borderBottomLeftRadius: 10,
+ borderTopRightRadius: 10,
+ borderTopLeftRadius: 10,
+ boxShadow: `0px 4px 6px ${colors.walletBoxShadow}`,
+ overflow: 'hidden',
+ },
+ list: {
+ padding: 0,
+ },
+ tokenItemInnerDiv: {
+ paddingLeft: 60,
+ },
+ headerItemInnerDiv: {
+ paddingLeft: 65,
+ },
+ footerItemInnerDiv: {
+ paddingLeft: 24,
+ },
+ borderedItem: {
+ borderBottomColor: colors.walletBorder,
+ borderBottomStyle: 'solid',
+ borderWidth: 1,
+ },
+ tokenItem: {
+ backgroundColor: colors.walletDefaultItemBackground,
+ paddingTop: 8,
+ paddingBottom: 8,
+ },
+ headerItem: {
+ paddingTop: 8,
+ paddingBottom: 8,
+ },
+ wrappedEtherButtonLabel: {
+ fontSize: 12,
+ },
+ amountLabel: {
+ fontWeight: 'bold',
+ color: colors.black,
+ },
+};
+
+const ETHER_ICON_PATH = '/images/ether.png';
+const ETHER_TOKEN_SYMBOL = 'WETH';
+const ZRX_TOKEN_SYMBOL = 'ZRX';
+const ETHER_SYMBOL = 'ETH';
+const ICON_DIMENSION = 24;
+const TOKEN_AMOUNT_DISPLAY_PRECISION = 3;
+
+export class Wallet extends React.Component<WalletProps, WalletState> {
+ private _isUnmounted: boolean;
+ constructor(props: WalletProps) {
+ super(props);
+ this._isUnmounted = false;
+ const initialTrackedTokenStateByAddress = this._getInitialTrackedTokenStateByAddress(props.trackedTokens);
+ this.state = {
+ trackedTokenStateByAddress: initialTrackedTokenStateByAddress,
+ };
+ }
+ public componentWillMount() {
+ const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress);
+ // tslint:disable-next-line:no-floating-promises
+ this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses);
+ }
+ public componentWillUnmount() {
+ this._isUnmounted = true;
+ }
+ public componentWillReceiveProps(nextProps: WalletProps) {
+ if (
+ nextProps.userAddress !== this.props.userAddress ||
+ nextProps.networkId !== this.props.networkId ||
+ nextProps.lastForceTokenStateRefetch !== this.props.lastForceTokenStateRefetch
+ ) {
+ const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress);
+ // tslint:disable-next-line:no-floating-promises
+ this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses);
+ }
+ if (!_.isEqual(nextProps.trackedTokens, this.props.trackedTokens)) {
+ const newTokens = _.difference(nextProps.trackedTokens, this.props.trackedTokens);
+ const newTokenAddresses = _.map(newTokens, token => token.address);
+ // Add placeholder entry for this token to the state, since fetching the
+ // balance/allowance is asynchronous
+ const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress;
+ _.each(newTokenAddresses, (tokenAddress: string) => {
+ trackedTokenStateByAddress[tokenAddress] = {
+ balance: new BigNumber(0),
+ allowance: new BigNumber(0),
+ isLoaded: false,
+ };
+ });
+ this.setState({
+ trackedTokenStateByAddress,
+ });
+ // Fetch the actual balance/allowance.
+ // tslint:disable-next-line:no-floating-promises
+ this._fetchBalancesAndAllowancesAsync(newTokenAddresses);
+ }
+ }
+ public render() {
+ const isReadyToRender = this.props.blockchainIsLoaded && this.props.blockchainErr === BlockchainErrs.NoError;
+ return <div style={styles.wallet}>{isReadyToRender && this._renderRows()}</div>;
+ }
+ private _renderRows() {
+ return (
+ <List style={styles.list}>
+ {_.concat(
+ this._renderHeaderRows(),
+ this._renderEthRows(),
+ this._renderTokenRows(),
+ this._renderFooterRows(),
+ )}
+ </List>
+ );
+ }
+ private _renderHeaderRows() {
+ const userAddress = this.props.userAddress;
+ const primaryText = utils.getAddressBeginAndEnd(userAddress);
+ return (
+ <ListItem
+ primaryText={primaryText}
+ leftIcon={<Identicon address={userAddress} diameter={ICON_DIMENSION} />}
+ style={{ ...styles.headerItem, ...styles.borderedItem }}
+ innerDivStyle={styles.headerItemInnerDiv}
+ />
+ );
+ }
+ private _renderFooterRows() {
+ const primaryText = '+ other tokens';
+ return (
+ <ListItem primaryText={primaryText} style={styles.borderedItem} innerDivStyle={styles.footerItemInnerDiv} />
+ );
+ }
+ private _renderEthRows() {
+ const primaryText = this._renderAmount(
+ this.props.userEtherBalanceInWei,
+ constants.DECIMAL_PLACES_ETH,
+ ETHER_SYMBOL,
+ );
+ const accessoryItemConfig = {
+ wrappedEtherAction: WrappedEtherAction.Wrap,
+ };
+ return (
+ <ListItem
+ primaryText={primaryText}
+ leftIcon={<img style={{ width: ICON_DIMENSION, height: ICON_DIMENSION }} src={ETHER_ICON_PATH} />}
+ rightAvatar={this._renderAccessoryItems(accessoryItemConfig)}
+ style={{ ...styles.tokenItem, ...styles.borderedItem }}
+ innerDivStyle={styles.tokenItemInnerDiv}
+ />
+ );
+ }
+ private _renderTokenRows() {
+ const trackedTokens = this.props.trackedTokens;
+ const trackedTokensStartingWithEtherToken = trackedTokens.sort(
+ firstBy((t: Token) => t.symbol !== ETHER_TOKEN_SYMBOL)
+ .thenBy((t: Token) => t.symbol !== ZRX_TOKEN_SYMBOL)
+ .thenBy('address'),
+ );
+ return _.map(trackedTokensStartingWithEtherToken, this._renderTokenRow.bind(this));
+ }
+ private _renderTokenRow(token: Token) {
+ const tokenState = this.state.trackedTokenStateByAddress[token.address];
+ const tokenLink = sharedUtils.getEtherScanLinkIfExists(
+ token.address,
+ this.props.networkId,
+ EtherscanLinkSuffixes.Address,
+ );
+ const amount = this._renderAmount(tokenState.balance, token.decimals, token.symbol);
+ const wrappedEtherAction = token.symbol === ETHER_TOKEN_SYMBOL ? WrappedEtherAction.Unwrap : undefined;
+ const accessoryItemConfig: AccessoryItemConfig = {
+ wrappedEtherAction,
+ allowanceToggleConfig: {
+ token,
+ tokenState,
+ },
+ };
+ return (
+ <ListItem
+ primaryText={amount}
+ leftIcon={this._renderTokenIcon(token, tokenLink)}
+ rightAvatar={this._renderAccessoryItems(accessoryItemConfig)}
+ style={{ ...styles.tokenItem, ...styles.borderedItem }}
+ innerDivStyle={styles.tokenItemInnerDiv}
+ />
+ );
+ }
+ private _renderAccessoryItems(config: AccessoryItemConfig) {
+ const shouldShowWrappedEtherAction = !_.isUndefined(config.wrappedEtherAction);
+ const shouldShowToggle = !_.isUndefined(config.allowanceToggleConfig);
+ return (
+ <div style={{ width: 160 }}>
+ <div className="flex">
+ <div className="flex-auto">
+ {shouldShowWrappedEtherAction && this._renderWrappedEtherButton(config.wrappedEtherAction)}
+ </div>
+ <div className="flex-last py1">
+ {shouldShowToggle && this._renderAllowanceToggle(config.allowanceToggleConfig)}
+ </div>
+ </div>
+ </div>
+ );
+ }
+ private _renderAllowanceToggle(config: AllowanceToggleConfig) {
+ return (
+ <AllowanceToggle
+ networkId={this.props.networkId}
+ blockchain={this.props.blockchain}
+ dispatcher={this.props.dispatcher}
+ token={config.token}
+ tokenState={config.tokenState}
+ onErrorOccurred={_.noop} // TODO: Error handling
+ userAddress={this.props.userAddress}
+ isDisabled={!config.tokenState.isLoaded}
+ refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this, config.token.address)}
+ />
+ );
+ }
+ private _renderAmount(amount: BigNumber, decimals: number, symbol: string) {
+ const unitAmount = ZeroEx.toUnitAmount(amount, decimals);
+ const formattedAmount = unitAmount.toPrecision(TOKEN_AMOUNT_DISPLAY_PRECISION);
+ const result = `${formattedAmount} ${symbol}`;
+ return <div style={styles.amountLabel}>{result}</div>;
+ }
+ private _renderTokenIcon(token: Token, tokenLink?: string) {
+ const tooltipId = `tooltip-${token.address}`;
+ const tokenIcon = <TokenIcon token={token} diameter={ICON_DIMENSION} />;
+ if (_.isUndefined(tokenLink)) {
+ return tokenIcon;
+ } else {
+ return (
+ <a href={tokenLink} target="_blank" style={{ textDecoration: 'none' }}>
+ {tokenIcon}
+ </a>
+ );
+ }
+ }
+ private _renderWrappedEtherButton(action: WrappedEtherAction) {
+ let buttonLabel;
+ let buttonIcon;
+ switch (action) {
+ case WrappedEtherAction.Wrap:
+ buttonLabel = 'wrap';
+ buttonIcon = <NavigationArrowDownward />;
+ break;
+ case WrappedEtherAction.Unwrap:
+ buttonLabel = 'unwrap';
+ buttonIcon = <NavigationArrowUpward />;
+ break;
+ default:
+ throw utils.spawnSwitchErr('wrappedEtherAction', action);
+ }
+ return (
+ <FlatButton
+ label={buttonLabel}
+ labelPosition="after"
+ primary={true}
+ icon={buttonIcon}
+ labelStyle={styles.wrappedEtherButtonLabel}
+ />
+ );
+ }
+ private _getInitialTrackedTokenStateByAddress(trackedTokens: Token[]) {
+ const trackedTokenStateByAddress: TokenStateByAddress = {};
+ _.each(trackedTokens, token => {
+ trackedTokenStateByAddress[token.address] = {
+ balance: new BigNumber(0),
+ allowance: new BigNumber(0),
+ isLoaded: false,
+ };
+ });
+ return trackedTokenStateByAddress;
+ }
+ private async _fetchBalancesAndAllowancesAsync(tokenAddresses: string[]) {
+ const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress;
+ const userAddressIfExists = _.isEmpty(this.props.userAddress) ? undefined : this.props.userAddress;
+ for (const tokenAddress of tokenAddresses) {
+ const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
+ userAddressIfExists,
+ tokenAddress,
+ );
+ trackedTokenStateByAddress[tokenAddress] = {
+ balance,
+ allowance,
+ isLoaded: true,
+ };
+ }
+ if (!this._isUnmounted) {
+ this.setState({
+ trackedTokenStateByAddress,
+ });
+ }
+ }
+ private async _refetchTokenStateAsync(tokenAddress: string) {
+ const userAddressIfExists = _.isEmpty(this.props.userAddress) ? undefined : this.props.userAddress;
+ const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
+ userAddressIfExists,
+ tokenAddress,
+ );
+ this.setState({
+ trackedTokenStateByAddress: {
+ ...this.state.trackedTokenStateByAddress,
+ [tokenAddress]: {
+ balance,
+ allowance,
+ isLoaded: true,
+ },
+ },
+ });
+ }
+}
diff --git a/packages/website/ts/types.ts b/packages/website/ts/types.ts
index b3e4973fa..c0a27a125 100644
--- a/packages/website/ts/types.ts
+++ b/packages/website/ts/types.ts
@@ -21,11 +21,6 @@ export interface TokenByAddress {
[address: string]: Token;
}
-export interface TokenState {
- allowance: BigNumber;
- balance: BigNumber;
-}
-
export interface AssetToken {
address?: string;
amount?: BigNumber;
@@ -469,4 +464,14 @@ export enum Providers {
Metamask = 'METAMASK',
Mist = 'MIST',
}
+
+export interface TokenStateByAddress {
+ [address: string]: TokenState;
+}
+
+export interface TokenState {
+ balance: BigNumber;
+ allowance: BigNumber;
+ isLoaded: boolean;
+}
// tslint:disable:max-file-line-count