593 lines
24 KiB
TypeScript
593 lines
24 KiB
TypeScript
import {
|
|
constants as sharedConstants,
|
|
EtherscanLinkSuffixes,
|
|
Styles,
|
|
utils as sharedUtils,
|
|
} from '@0xproject/react-shared';
|
|
import { BigNumber } from '@0xproject/utils';
|
|
import { Web3Wrapper } from '@0xproject/web3-wrapper';
|
|
import * as _ from 'lodash';
|
|
import FlatButton from 'material-ui/FlatButton';
|
|
import FloatingActionButton from 'material-ui/FloatingActionButton';
|
|
import { ListItem } from 'material-ui/List';
|
|
import ActionAccountBalanceWallet from 'material-ui/svg-icons/action/account-balance-wallet';
|
|
import ContentAdd from 'material-ui/svg-icons/content/add';
|
|
import ContentRemove from 'material-ui/svg-icons/content/remove';
|
|
import NavigationArrowDownward from 'material-ui/svg-icons/navigation/arrow-downward';
|
|
import NavigationArrowUpward from 'material-ui/svg-icons/navigation/arrow-upward';
|
|
import Close from 'material-ui/svg-icons/navigation/close';
|
|
import * as React from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import ReactTooltip = require('react-tooltip');
|
|
import firstBy = require('thenby');
|
|
|
|
import { Blockchain } from 'ts/blockchain';
|
|
import { AllowanceToggle } from 'ts/components/inputs/allowance_toggle';
|
|
import { IconButton } from 'ts/components/ui/icon_button';
|
|
import { Identicon } from 'ts/components/ui/identicon';
|
|
import { Island } from 'ts/components/ui/island';
|
|
import { TokenIcon } from 'ts/components/ui/token_icon';
|
|
import { WalletDisconnectedItem } from 'ts/components/wallet/wallet_disconnected_item';
|
|
import { WrapEtherItem } from 'ts/components/wallet/wrap_ether_item';
|
|
import { Dispatcher } from 'ts/redux/dispatcher';
|
|
import { colors } from 'ts/style/colors';
|
|
import { zIndex } from 'ts/style/z_index';
|
|
import {
|
|
BalanceErrs,
|
|
BlockchainErrs,
|
|
ItemByAddress,
|
|
ProviderType,
|
|
ScreenWidths,
|
|
Side,
|
|
Token,
|
|
TokenByAddress,
|
|
TokenState,
|
|
TokenStateByAddress,
|
|
WebsitePaths,
|
|
} from 'ts/types';
|
|
import { backendClient } from 'ts/utils/backend_client';
|
|
import { constants } from 'ts/utils/constants';
|
|
import { utils } from 'ts/utils/utils';
|
|
import { styles as walletItemStyles } from 'ts/utils/wallet_item_styles';
|
|
|
|
export interface WalletProps {
|
|
userAddress: string;
|
|
networkId: number;
|
|
blockchain: Blockchain;
|
|
blockchainIsLoaded: boolean;
|
|
blockchainErr: BlockchainErrs;
|
|
dispatcher: Dispatcher;
|
|
tokenByAddress: TokenByAddress;
|
|
trackedTokens: Token[];
|
|
userEtherBalanceInWei: BigNumber;
|
|
lastForceTokenStateRefetch: number;
|
|
injectedProviderName: string;
|
|
providerType: ProviderType;
|
|
screenWidth: ScreenWidths;
|
|
location: Location;
|
|
onToggleLedgerDialog: () => void;
|
|
onAddToken: () => void;
|
|
onRemoveToken: () => void;
|
|
}
|
|
|
|
interface WalletState {
|
|
trackedTokenStateByAddress: TokenStateByAddress;
|
|
wrappedEtherDirection?: Side;
|
|
isHoveringSidebar: boolean;
|
|
}
|
|
|
|
interface AllowanceToggleConfig {
|
|
token: Token;
|
|
tokenState: TokenState;
|
|
}
|
|
|
|
interface AccessoryItemConfig {
|
|
wrappedEtherDirection?: Side;
|
|
allowanceToggleConfig?: AllowanceToggleConfig;
|
|
}
|
|
|
|
const styles: Styles = {
|
|
root: {
|
|
width: '100%',
|
|
zIndex: zIndex.aboveOverlay,
|
|
position: 'relative',
|
|
},
|
|
headerItemInnerDiv: {
|
|
paddingLeft: 65,
|
|
},
|
|
footerItemInnerDiv: {
|
|
paddingLeft: 24,
|
|
borderTopColor: colors.walletBorder,
|
|
borderTopStyle: 'solid',
|
|
borderWidth: 1,
|
|
},
|
|
borderedItem: {
|
|
borderBottomColor: colors.walletBorder,
|
|
borderBottomStyle: 'solid',
|
|
borderWidth: 1,
|
|
},
|
|
tokenItem: {
|
|
backgroundColor: colors.walletDefaultItemBackground,
|
|
},
|
|
amountLabel: {
|
|
fontWeight: 'bold',
|
|
color: colors.black,
|
|
},
|
|
valueLabel: {
|
|
color: colors.grey,
|
|
fontSize: 14,
|
|
},
|
|
paddedItem: {
|
|
paddingTop: 8,
|
|
paddingBottom: 8,
|
|
},
|
|
bodyInnerDiv: {
|
|
overflow: 'auto',
|
|
WebkitOverflowScrolling: 'touch',
|
|
},
|
|
manageYourWalletText: {
|
|
color: colors.mediumBlue,
|
|
fontWeight: 'bold',
|
|
},
|
|
};
|
|
|
|
const ETHER_ICON_PATH = '/images/ether.png';
|
|
const ICON_DIMENSION = 24;
|
|
const TOKEN_AMOUNT_DISPLAY_PRECISION = 3;
|
|
const BODY_ITEM_KEY = 'BODY';
|
|
const HEADER_ITEM_KEY = 'HEADER';
|
|
const FOOTER_ITEM_KEY = 'FOOTER';
|
|
const DISCONNECTED_ITEM_KEY = 'DISCONNECTED';
|
|
const ETHER_ITEM_KEY = 'ETHER';
|
|
const USD_DECIMAL_PLACES = 2;
|
|
const NO_ALLOWANCE_TOGGLE_SPACE_WIDTH = 56;
|
|
const ACCOUNT_PATH = `${WebsitePaths.Portal}/account`;
|
|
|
|
export class Wallet extends React.Component<WalletProps, WalletState> {
|
|
private _isUnmounted: boolean;
|
|
constructor(props: WalletProps) {
|
|
super(props);
|
|
this._isUnmounted = false;
|
|
const trackedTokenAddresses = _.map(props.trackedTokens, token => token.address);
|
|
const initialTrackedTokenStateByAddress = this._getInitialTrackedTokenStateByAddress(trackedTokenAddresses);
|
|
this.state = {
|
|
trackedTokenStateByAddress: initialTrackedTokenStateByAddress,
|
|
wrappedEtherDirection: undefined,
|
|
isHoveringSidebar: false,
|
|
};
|
|
}
|
|
public componentWillMount(): void {
|
|
const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress);
|
|
// tslint:disable-next-line:no-floating-promises
|
|
this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses);
|
|
}
|
|
public componentWillUnmount(): void {
|
|
this._isUnmounted = true;
|
|
}
|
|
public componentWillReceiveProps(nextProps: WalletProps): void {
|
|
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;
|
|
const initialTrackedTokenStateByAddress = this._getInitialTrackedTokenStateByAddress(newTokenAddresses);
|
|
_.assign(trackedTokenStateByAddress, initialTrackedTokenStateByAddress);
|
|
this.setState({
|
|
trackedTokenStateByAddress,
|
|
});
|
|
// Fetch the actual balance/allowance.
|
|
// tslint:disable-next-line:no-floating-promises
|
|
this._fetchBalancesAndAllowancesAsync(newTokenAddresses);
|
|
}
|
|
}
|
|
public render(): React.ReactNode {
|
|
const isReadyToRender = this.props.blockchainIsLoaded && this.props.blockchainErr === BlockchainErrs.NoError;
|
|
const isAddressAvailable = !_.isEmpty(this.props.userAddress);
|
|
return (
|
|
<Island className="flex flex-column wallet" style={styles.root}>
|
|
{isReadyToRender && isAddressAvailable
|
|
? _.concat(this._renderConnectedHeaderRows(), this._renderBody(), this._renderFooterRows())
|
|
: _.concat(this._renderDisconnectedHeaderRows(), this._renderDisconnectedRows())}
|
|
</Island>
|
|
);
|
|
}
|
|
private _renderDisconnectedHeaderRows(): React.ReactElement<{}> {
|
|
const userAddress = this.props.userAddress;
|
|
const primaryText = 'wallet';
|
|
return (
|
|
<ListItem
|
|
key={HEADER_ITEM_KEY}
|
|
primaryText={primaryText.toUpperCase()}
|
|
leftIcon={<ActionAccountBalanceWallet color={colors.mediumBlue} />}
|
|
style={styles.paddedItem}
|
|
innerDivStyle={styles.headerItemInnerDiv}
|
|
/>
|
|
);
|
|
}
|
|
private _renderDisconnectedRows(): React.ReactElement<{}> {
|
|
return (
|
|
<WalletDisconnectedItem
|
|
key={DISCONNECTED_ITEM_KEY}
|
|
providerType={this.props.providerType}
|
|
injectedProviderName={this.props.injectedProviderName}
|
|
onToggleLedgerDialog={this.props.onToggleLedgerDialog}
|
|
/>
|
|
);
|
|
}
|
|
private _renderConnectedHeaderRows(): React.ReactElement<{}> {
|
|
const userAddress = this.props.userAddress;
|
|
const primaryText = utils.getAddressBeginAndEnd(userAddress);
|
|
return (
|
|
<Link key={HEADER_ITEM_KEY} to={ACCOUNT_PATH} style={{ textDecoration: 'none' }}>
|
|
<ListItem
|
|
primaryText={primaryText}
|
|
leftIcon={<Identicon address={userAddress} diameter={ICON_DIMENSION} />}
|
|
style={{ ...styles.paddedItem, ...styles.borderedItem }}
|
|
innerDivStyle={styles.headerItemInnerDiv}
|
|
/>
|
|
</Link>
|
|
);
|
|
}
|
|
private _renderBody(): React.ReactElement<{}> {
|
|
const bodyStyle: React.CSSProperties = {
|
|
...styles.bodyInnerDiv,
|
|
overflow: this.state.isHoveringSidebar ? 'auto' : 'hidden',
|
|
// TODO: make this completely responsive
|
|
maxHeight: this.props.screenWidth !== ScreenWidths.Sm ? 475 : undefined,
|
|
};
|
|
return (
|
|
<div
|
|
style={bodyStyle}
|
|
key={BODY_ITEM_KEY}
|
|
onMouseEnter={this._onSidebarHover.bind(this)}
|
|
onMouseLeave={this._onSidebarHoverOff.bind(this)}
|
|
>
|
|
{this._renderEthRows()}
|
|
{this._renderTokenRows()}
|
|
</div>
|
|
);
|
|
}
|
|
private _onSidebarHover(event: React.FormEvent<HTMLInputElement>): void {
|
|
this.setState({
|
|
isHoveringSidebar: true,
|
|
});
|
|
}
|
|
private _onSidebarHoverOff(): void {
|
|
this.setState({
|
|
isHoveringSidebar: false,
|
|
});
|
|
}
|
|
private _renderFooterRows(): React.ReactElement<{}> {
|
|
return (
|
|
<div key={FOOTER_ITEM_KEY}>
|
|
<ListItem
|
|
primaryText={
|
|
<div className="flex">
|
|
<FloatingActionButton mini={true} zDepth={0} onClick={this.props.onAddToken}>
|
|
<ContentAdd />
|
|
</FloatingActionButton>
|
|
<FloatingActionButton
|
|
mini={true}
|
|
zDepth={0}
|
|
className="px1"
|
|
onClick={this.props.onRemoveToken}
|
|
>
|
|
<ContentRemove />
|
|
</FloatingActionButton>
|
|
<div
|
|
style={{
|
|
paddingLeft: 10,
|
|
position: 'relative',
|
|
top: '50%',
|
|
transform: 'translateY(33%)',
|
|
}}
|
|
>
|
|
add/remove tokens
|
|
</div>
|
|
</div>
|
|
}
|
|
disabled={true}
|
|
innerDivStyle={styles.footerItemInnerDiv}
|
|
style={styles.borderedItem}
|
|
/>
|
|
{this.props.location.pathname !== ACCOUNT_PATH && (
|
|
<Link to={ACCOUNT_PATH} style={{ textDecoration: 'none' }}>
|
|
<ListItem
|
|
primaryText={
|
|
<div className="flex right" style={styles.manageYourWalletText}>
|
|
{'manage your wallet'}
|
|
</div>
|
|
// https://github.com/palantir/tslint-react/issues/140
|
|
// tslint:disable-next-line:jsx-curly-spacing
|
|
}
|
|
style={{ ...styles.paddedItem, ...styles.borderedItem }}
|
|
/>
|
|
</Link>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
private _renderEthRows(): React.ReactNode {
|
|
const icon = <img style={{ width: ICON_DIMENSION, height: ICON_DIMENSION }} src={ETHER_ICON_PATH} />;
|
|
const primaryText = this._renderAmount(
|
|
this.props.userEtherBalanceInWei,
|
|
constants.DECIMAL_PLACES_ETH,
|
|
constants.ETHER_SYMBOL,
|
|
);
|
|
const etherToken = this._getEthToken();
|
|
const etherPrice = this.state.trackedTokenStateByAddress[etherToken.address].price;
|
|
const secondaryText = this._renderValue(
|
|
this.props.userEtherBalanceInWei,
|
|
constants.DECIMAL_PLACES_ETH,
|
|
etherPrice,
|
|
);
|
|
const accessoryItemConfig = {
|
|
wrappedEtherDirection: Side.Deposit,
|
|
};
|
|
const isInWrappedEtherState =
|
|
!_.isUndefined(this.state.wrappedEtherDirection) &&
|
|
this.state.wrappedEtherDirection === accessoryItemConfig.wrappedEtherDirection;
|
|
const style = isInWrappedEtherState
|
|
? { ...walletItemStyles.focusedItem, ...styles.paddedItem }
|
|
: { ...styles.tokenItem, ...styles.borderedItem, ...styles.paddedItem };
|
|
const key = ETHER_ITEM_KEY;
|
|
return this._renderBalanceRow(key, icon, primaryText, secondaryText, accessoryItemConfig, 'eth-row');
|
|
}
|
|
private _renderTokenRows(): React.ReactNode {
|
|
const trackedTokens = this.props.trackedTokens;
|
|
const trackedTokensStartingWithEtherToken = trackedTokens.sort(
|
|
firstBy((t: Token) => t.symbol !== constants.ETHER_TOKEN_SYMBOL)
|
|
.thenBy((t: Token) => t.symbol !== constants.ZRX_TOKEN_SYMBOL)
|
|
.thenBy('address'),
|
|
);
|
|
return _.map(trackedTokensStartingWithEtherToken, this._renderTokenRow.bind(this));
|
|
}
|
|
private _renderTokenRow(token: Token, index: number): React.ReactNode {
|
|
const tokenState = this.state.trackedTokenStateByAddress[token.address];
|
|
const tokenLink = sharedUtils.getEtherScanLinkIfExists(
|
|
token.address,
|
|
this.props.networkId,
|
|
EtherscanLinkSuffixes.Address,
|
|
);
|
|
const icon = <TokenIcon token={token} diameter={ICON_DIMENSION} link={tokenLink} />;
|
|
const primaryText = this._renderAmount(tokenState.balance, token.decimals, token.symbol);
|
|
const secondaryText = this._renderValue(tokenState.balance, token.decimals, tokenState.price);
|
|
const isWeth = token.symbol === constants.ETHER_TOKEN_SYMBOL;
|
|
const wrappedEtherDirection = isWeth ? Side.Receive : undefined;
|
|
const accessoryItemConfig: AccessoryItemConfig = {
|
|
wrappedEtherDirection,
|
|
allowanceToggleConfig: {
|
|
token,
|
|
tokenState,
|
|
},
|
|
};
|
|
const key = token.address;
|
|
return this._renderBalanceRow(
|
|
key,
|
|
icon,
|
|
primaryText,
|
|
secondaryText,
|
|
accessoryItemConfig,
|
|
isWeth ? 'weth-row' : undefined,
|
|
);
|
|
}
|
|
private _renderBalanceRow(
|
|
key: string,
|
|
icon: React.ReactNode,
|
|
primaryText: React.ReactNode,
|
|
secondaryText: React.ReactNode,
|
|
accessoryItemConfig: AccessoryItemConfig,
|
|
className?: string,
|
|
): React.ReactNode {
|
|
const shouldShowWrapEtherItem =
|
|
!_.isUndefined(this.state.wrappedEtherDirection) &&
|
|
this.state.wrappedEtherDirection === accessoryItemConfig.wrappedEtherDirection;
|
|
const style = shouldShowWrapEtherItem
|
|
? { ...walletItemStyles.focusedItem, ...styles.paddedItem }
|
|
: { ...styles.tokenItem, ...styles.borderedItem, ...styles.paddedItem };
|
|
const etherToken = this._getEthToken();
|
|
return (
|
|
<div key={key} className={`flex flex-column ${className || ''}`}>
|
|
<div className="flex items-center" style={style}>
|
|
<div className="px2">{icon}</div>
|
|
<div className="flex-none pr2 pt2 pb2">
|
|
{primaryText}
|
|
{secondaryText}
|
|
</div>
|
|
<div className="flex-auto" />
|
|
<div>{this._renderAccessoryItems(accessoryItemConfig)}</div>
|
|
</div>
|
|
{shouldShowWrapEtherItem && (
|
|
<WrapEtherItem
|
|
userAddress={this.props.userAddress}
|
|
networkId={this.props.networkId}
|
|
blockchain={this.props.blockchain}
|
|
dispatcher={this.props.dispatcher}
|
|
userEtherBalanceInWei={this.props.userEtherBalanceInWei}
|
|
direction={accessoryItemConfig.wrappedEtherDirection}
|
|
etherToken={etherToken}
|
|
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
|
|
onConversionSuccessful={this._closeWrappedEtherActionRow.bind(this)}
|
|
refetchEthTokenStateAsync={this._refetchTokenStateAsync.bind(this, etherToken.address)}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
private _renderAccessoryItems(config: AccessoryItemConfig): React.ReactElement<{}> {
|
|
const shouldShowWrappedEtherAction = !_.isUndefined(config.wrappedEtherDirection);
|
|
const shouldShowToggle = !_.isUndefined(config.allowanceToggleConfig);
|
|
// if we don't have a toggle, we still want some space to the right of the "wrap" button so that it aligns with
|
|
// the "unwrap" button in the row below
|
|
const toggle = shouldShowToggle ? (
|
|
this._renderAllowanceToggle(config.allowanceToggleConfig)
|
|
) : (
|
|
<div style={{ width: NO_ALLOWANCE_TOGGLE_SPACE_WIDTH }} />
|
|
);
|
|
return (
|
|
<div className="flex items-center">
|
|
<div className="flex-auto">
|
|
{shouldShowWrappedEtherAction && this._renderWrappedEtherButton(config.wrappedEtherDirection)}
|
|
</div>
|
|
<div className="flex-last pl2">{toggle}</div>
|
|
</div>
|
|
);
|
|
}
|
|
private _renderAllowanceToggle(config: AllowanceToggleConfig): React.ReactNode {
|
|
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): React.ReactNode {
|
|
const unitAmount = Web3Wrapper.toUnitAmount(amount, decimals);
|
|
const formattedAmount = unitAmount.toPrecision(TOKEN_AMOUNT_DISPLAY_PRECISION);
|
|
const result = `${formattedAmount} ${symbol}`;
|
|
return <div style={styles.amountLabel}>{result}</div>;
|
|
}
|
|
private _renderValue(amount: BigNumber, decimals: number, price?: BigNumber): React.ReactNode {
|
|
if (_.isUndefined(price)) {
|
|
return null;
|
|
}
|
|
const unitAmount = Web3Wrapper.toUnitAmount(amount, decimals);
|
|
const value = unitAmount.mul(price);
|
|
const formattedAmount = value.toFixed(USD_DECIMAL_PLACES);
|
|
const result = `$${formattedAmount}`;
|
|
return <div style={styles.valueLabel}>{result}</div>;
|
|
}
|
|
private _renderWrappedEtherButton(wrappedEtherDirection: Side): React.ReactNode {
|
|
const isWrappedEtherDirectionOpen = this.state.wrappedEtherDirection === wrappedEtherDirection;
|
|
let buttonLabel;
|
|
let buttonIconName;
|
|
if (isWrappedEtherDirectionOpen) {
|
|
buttonLabel = 'cancel';
|
|
buttonIconName = 'zmdi-close';
|
|
} else {
|
|
switch (wrappedEtherDirection) {
|
|
case Side.Deposit:
|
|
buttonLabel = 'wrap';
|
|
buttonIconName = 'zmdi-long-arrow-down';
|
|
break;
|
|
case Side.Receive:
|
|
buttonLabel = 'unwrap';
|
|
buttonIconName = 'zmdi-long-arrow-up';
|
|
break;
|
|
default:
|
|
throw utils.spawnSwitchErr('wrappedEtherDirection', wrappedEtherDirection);
|
|
}
|
|
}
|
|
const onClick = isWrappedEtherDirectionOpen
|
|
? this._closeWrappedEtherActionRow.bind(this)
|
|
: this._openWrappedEtherActionRow.bind(this, wrappedEtherDirection);
|
|
return (
|
|
<IconButton iconName={buttonIconName} labelText={buttonLabel} onClick={onClick} color={colors.mediumBlue} />
|
|
);
|
|
}
|
|
private _getInitialTrackedTokenStateByAddress(tokenAddresses: string[]): TokenStateByAddress {
|
|
const trackedTokenStateByAddress: TokenStateByAddress = {};
|
|
_.each(tokenAddresses, tokenAddress => {
|
|
trackedTokenStateByAddress[tokenAddress] = {
|
|
balance: new BigNumber(0),
|
|
allowance: new BigNumber(0),
|
|
isLoaded: false,
|
|
};
|
|
});
|
|
return trackedTokenStateByAddress;
|
|
}
|
|
private async _fetchBalancesAndAllowancesAsync(tokenAddresses: string[]): Promise<void> {
|
|
const balanceAndAllowanceTupleByAddress: ItemByAddress<BigNumber[]> = {};
|
|
const userAddressIfExists = _.isEmpty(this.props.userAddress) ? undefined : this.props.userAddress;
|
|
for (const tokenAddress of tokenAddresses) {
|
|
const balanceAndAllowanceTuple = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
|
|
userAddressIfExists,
|
|
tokenAddress,
|
|
);
|
|
balanceAndAllowanceTupleByAddress[tokenAddress] = balanceAndAllowanceTuple;
|
|
}
|
|
const priceByAddress = await this._getPriceByAddressAsync(tokenAddresses);
|
|
const trackedTokenStateByAddress = _.reduce(
|
|
tokenAddresses,
|
|
(acc, address) => {
|
|
const [balance, allowance] = balanceAndAllowanceTupleByAddress[address];
|
|
const priceIfExists = _.get(priceByAddress, address);
|
|
acc[address] = {
|
|
balance,
|
|
allowance,
|
|
price: priceIfExists,
|
|
isLoaded: true,
|
|
};
|
|
return acc;
|
|
},
|
|
this.state.trackedTokenStateByAddress,
|
|
);
|
|
|
|
if (!this._isUnmounted) {
|
|
this.setState({
|
|
trackedTokenStateByAddress,
|
|
});
|
|
}
|
|
}
|
|
private async _refetchTokenStateAsync(tokenAddress: string): Promise<void> {
|
|
await this._fetchBalancesAndAllowancesAsync([tokenAddress]);
|
|
}
|
|
private async _getPriceByAddressAsync(tokenAddresses: string[]): Promise<ItemByAddress<BigNumber>> {
|
|
if (_.isEmpty(tokenAddresses)) {
|
|
return {};
|
|
}
|
|
// for each input token address, search for the corresponding symbol in this.props.tokenByAddress, if it exists
|
|
// create a mapping from existing symbols -> address
|
|
const tokenAddressBySymbol: { [symbol: string]: string } = {};
|
|
_.each(tokenAddresses, address => {
|
|
const tokenIfExists = _.get(this.props.tokenByAddress, address);
|
|
if (!_.isUndefined(tokenIfExists)) {
|
|
const symbol = tokenIfExists.symbol;
|
|
tokenAddressBySymbol[symbol] = address;
|
|
}
|
|
});
|
|
const tokenSymbols = _.keys(tokenAddressBySymbol);
|
|
try {
|
|
const priceBySymbol = await backendClient.getPriceInfoAsync(tokenSymbols);
|
|
const priceByAddress = _.mapKeys(priceBySymbol, (value, symbol) => _.get(tokenAddressBySymbol, symbol));
|
|
const result = _.mapValues(priceByAddress, price => {
|
|
const priceBigNumber = new BigNumber(price);
|
|
return priceBigNumber;
|
|
});
|
|
return result;
|
|
} catch (err) {
|
|
return {};
|
|
}
|
|
}
|
|
private _openWrappedEtherActionRow(wrappedEtherDirection: Side): void {
|
|
this.setState({
|
|
wrappedEtherDirection,
|
|
});
|
|
}
|
|
private _closeWrappedEtherActionRow(): void {
|
|
this.setState({
|
|
wrappedEtherDirection: undefined,
|
|
});
|
|
}
|
|
private _getEthToken(): Token {
|
|
return utils.getEthToken(this.props.tokenByAddress);
|
|
}
|
|
} // tslint:disable:max-file-line-count
|