742 lines
32 KiB
TypeScript
742 lines
32 KiB
TypeScript
import { colors, Link } from '@0x/react-shared';
|
|
import { BigNumber } from '@0x/utils';
|
|
import * as _ from 'lodash';
|
|
import * as React from 'react';
|
|
import * as DocumentTitle from 'react-document-title';
|
|
import { Route, RouteComponentProps, Switch } from 'react-router-dom';
|
|
|
|
import { Blockchain } from 'ts/blockchain';
|
|
import { BlockchainErrDialog } from 'ts/components/dialogs/blockchain_err_dialog';
|
|
import { LedgerConfigDialog } from 'ts/components/dialogs/ledger_config_dialog';
|
|
import { PortalDisclaimerDialog } from 'ts/components/dialogs/portal_disclaimer_dialog';
|
|
import { EthWrappers } from 'ts/components/eth_wrappers';
|
|
import { FillOrder } from 'ts/components/fill_order';
|
|
import { AssetPicker } from 'ts/components/generate_order/asset_picker';
|
|
import { MetaTags } from 'ts/components/meta_tags';
|
|
import { BackButton } from 'ts/components/portal/back_button';
|
|
import { Loading } from 'ts/components/portal/loading';
|
|
import { Menu, MenuTheme } from 'ts/components/portal/menu';
|
|
import { Section } from 'ts/components/portal/section';
|
|
import { TextHeader } from 'ts/components/portal/text_header';
|
|
import { RelayerIndex, RelayerIndexCellStyle } from 'ts/components/relayer_index/relayer_index';
|
|
import { TokenBalances } from 'ts/components/token_balances';
|
|
import { TopBar, TopBarDisplayType } from 'ts/components/top_bar/top_bar';
|
|
import { TradeHistory } from 'ts/components/trade_history/trade_history';
|
|
import { Container } from 'ts/components/ui/container';
|
|
import { FlashMessage } from 'ts/components/ui/flash_message';
|
|
import { Image } from 'ts/components/ui/image';
|
|
import { PointerDirection } from 'ts/components/ui/pointer';
|
|
import { Text } from 'ts/components/ui/text';
|
|
import { Wallet } from 'ts/components/wallet/wallet';
|
|
import { GenerateOrderForm } from 'ts/containers/generate_order_form';
|
|
import { PortalOnboardingFlow } from 'ts/containers/portal_onboarding_flow';
|
|
import { localStorage } from 'ts/local_storage/local_storage';
|
|
import { trackedTokenStorage } from 'ts/local_storage/tracked_token_storage';
|
|
import { FullscreenMessage } from 'ts/pages/fullscreen_message';
|
|
import { Dispatcher } from 'ts/redux/dispatcher';
|
|
import { zIndex } from 'ts/style/z_index';
|
|
import {
|
|
BlockchainErrs,
|
|
HashData,
|
|
ItemByAddress,
|
|
PortalOrder,
|
|
ProviderType,
|
|
ScreenWidths,
|
|
Token,
|
|
TokenByAddress,
|
|
TokenStateByAddress,
|
|
TokenVisibility,
|
|
WebsitePaths,
|
|
} from 'ts/types';
|
|
import { analytics } from 'ts/utils/analytics';
|
|
import { backendClient } from 'ts/utils/backend_client';
|
|
import { configs } from 'ts/utils/configs';
|
|
import { constants } from 'ts/utils/constants';
|
|
import { orderParser } from 'ts/utils/order_parser';
|
|
import { Translate } from 'ts/utils/translate';
|
|
import { utils } from 'ts/utils/utils';
|
|
|
|
export interface PortalProps {
|
|
blockchainErr: BlockchainErrs;
|
|
blockchainIsLoaded: boolean;
|
|
dispatcher: Dispatcher;
|
|
hashData: HashData;
|
|
injectedProviderName: string;
|
|
networkId: number;
|
|
nodeVersion: string;
|
|
orderFillAmount: BigNumber;
|
|
providerType: ProviderType;
|
|
screenWidth: ScreenWidths;
|
|
tokenByAddress: TokenByAddress;
|
|
userEtherBalanceInWei?: BigNumber;
|
|
userAddress: string;
|
|
shouldBlockchainErrDialogBeOpen: boolean;
|
|
userSuppliedOrderCache: PortalOrder;
|
|
location: Location;
|
|
flashMessage?: string | React.ReactNode;
|
|
lastForceTokenStateRefetch: number;
|
|
translate: Translate;
|
|
isPortalOnboardingShowing: boolean;
|
|
portalOnboardingStep: number;
|
|
}
|
|
|
|
interface PortalState {
|
|
prevNetworkId: number;
|
|
prevNodeVersion: string;
|
|
prevUserAddress: string;
|
|
prevPathname: string;
|
|
isDisclaimerDialogOpen: boolean;
|
|
isLedgerDialogOpen: boolean;
|
|
tokenManagementState: TokenManagementState;
|
|
trackedTokenStateByAddress: TokenStateByAddress;
|
|
}
|
|
|
|
interface AccountManagementItem {
|
|
pathName: string;
|
|
headerText?: string;
|
|
render: () => React.ReactNode;
|
|
}
|
|
|
|
enum TokenManagementState {
|
|
Add = 'Add',
|
|
Remove = 'Remove',
|
|
None = 'None',
|
|
}
|
|
|
|
const THROTTLE_TIMEOUT = 100;
|
|
const TOP_BAR_HEIGHT = TopBar.heightForDisplayType(TopBarDisplayType.Expanded);
|
|
const LEFT_COLUMN_WIDTH = 346;
|
|
const MENU_PADDING_LEFT = 185;
|
|
const LARGE_LAYOUT_MAX_WIDTH = 1200;
|
|
const SIDE_PADDING = 20;
|
|
const DOCUMENT_TITLE = '0x Portal';
|
|
const DOCUMENT_DESCRIPTION = 'Learn about and trade on 0x Relayers';
|
|
|
|
export class Portal extends React.Component<PortalProps, PortalState> {
|
|
private _blockchain: Blockchain;
|
|
private readonly _sharedOrderIfExists: PortalOrder;
|
|
private readonly _throttledScreenWidthUpdate: () => void;
|
|
constructor(props: PortalProps) {
|
|
super(props);
|
|
this._sharedOrderIfExists = orderParser.parseQueryString(window.location.search);
|
|
this._throttledScreenWidthUpdate = _.throttle(this._updateScreenWidth.bind(this), THROTTLE_TIMEOUT);
|
|
const didAcceptPortalDisclaimer = localStorage.getItemIfExists(constants.LOCAL_STORAGE_KEY_ACCEPT_DISCLAIMER);
|
|
const hasAcceptedDisclaimer =
|
|
!_.isUndefined(didAcceptPortalDisclaimer) && !_.isEmpty(didAcceptPortalDisclaimer);
|
|
const initialTrackedTokenStateByAddress = this._getInitialTrackedTokenStateByAddress(
|
|
this._getCurrentTrackedTokens(),
|
|
);
|
|
this.state = {
|
|
prevNetworkId: this.props.networkId,
|
|
prevNodeVersion: this.props.nodeVersion,
|
|
prevUserAddress: this.props.userAddress,
|
|
prevPathname: this.props.location.pathname,
|
|
isDisclaimerDialogOpen: !hasAcceptedDisclaimer,
|
|
tokenManagementState: TokenManagementState.None,
|
|
isLedgerDialogOpen: false,
|
|
trackedTokenStateByAddress: initialTrackedTokenStateByAddress,
|
|
};
|
|
}
|
|
public componentDidMount(): void {
|
|
window.addEventListener('resize', this._throttledScreenWidthUpdate);
|
|
window.scrollTo(0, 0);
|
|
}
|
|
public componentWillMount(): void {
|
|
this._blockchain = new Blockchain(this.props.dispatcher);
|
|
}
|
|
public componentWillUnmount(): void {
|
|
this._blockchain.destroy();
|
|
window.removeEventListener('resize', this._throttledScreenWidthUpdate);
|
|
// We re-set the entire redux state when the portal is unmounted so that when it is re-rendered
|
|
// the initialization process always occurs from the same base state. This helps avoid
|
|
// initialization inconsistencies (i.e While the portal was unrendered, the user might have
|
|
// become disconnected from their backing Ethereum node, changed user accounts, etc...)
|
|
this.props.dispatcher.resetState();
|
|
}
|
|
public componentDidUpdate(prevProps: PortalProps): void {
|
|
if (!prevProps.blockchainIsLoaded && this.props.blockchainIsLoaded) {
|
|
// tslint:disable-next-line:no-floating-promises
|
|
this._fetchBalancesAndAllowancesAsync(this._getCurrentTrackedTokensAddresses());
|
|
}
|
|
}
|
|
public componentWillReceiveProps(nextProps: PortalProps): void {
|
|
if (nextProps.networkId !== this.state.prevNetworkId) {
|
|
// tslint:disable-next-line:no-floating-promises
|
|
this._blockchain.networkIdUpdatedFireAndForgetAsync(nextProps.networkId);
|
|
this.setState({
|
|
prevNetworkId: nextProps.networkId,
|
|
});
|
|
}
|
|
if (nextProps.userAddress !== this.state.prevUserAddress) {
|
|
const newUserAddress = _.isEmpty(nextProps.userAddress) ? undefined : nextProps.userAddress;
|
|
// tslint:disable-next-line:no-floating-promises
|
|
this._blockchain.userAddressUpdatedFireAndForgetAsync(newUserAddress);
|
|
this.setState({
|
|
prevUserAddress: nextProps.userAddress,
|
|
});
|
|
}
|
|
if (nextProps.nodeVersion !== this.state.prevNodeVersion) {
|
|
// tslint:disable-next-line:no-floating-promises
|
|
this._blockchain.nodeVersionUpdatedFireAndForgetAsync(nextProps.nodeVersion);
|
|
}
|
|
if (nextProps.location.pathname !== this.state.prevPathname) {
|
|
this.setState({
|
|
prevPathname: nextProps.location.pathname,
|
|
});
|
|
}
|
|
|
|
// If the address changed, but the network did not, we can just refetch the currently tracked tokens.
|
|
if (
|
|
(nextProps.userAddress !== this.props.userAddress && nextProps.networkId === this.props.networkId) ||
|
|
nextProps.lastForceTokenStateRefetch !== this.props.lastForceTokenStateRefetch
|
|
) {
|
|
// tslint:disable-next-line:no-floating-promises
|
|
this._fetchBalancesAndAllowancesAsync(this._getCurrentTrackedTokensAddresses());
|
|
}
|
|
|
|
const nextTrackedTokens = utils.getTrackedTokens(nextProps.tokenByAddress);
|
|
const trackedTokens = this._getCurrentTrackedTokens();
|
|
|
|
if (!_.isEqual(nextTrackedTokens, trackedTokens)) {
|
|
const newTokens = _.difference(nextTrackedTokens, 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 };
|
|
for (const tokenAddress of newTokenAddresses) {
|
|
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(): React.ReactNode {
|
|
const updateShouldBlockchainErrDialogBeOpen = this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen.bind(
|
|
this.props.dispatcher,
|
|
);
|
|
const isAssetPickerDialogOpen = this.state.tokenManagementState !== TokenManagementState.None;
|
|
const tokenVisibility =
|
|
this.state.tokenManagementState === TokenManagementState.Add
|
|
? TokenVisibility.UNTRACKED
|
|
: TokenVisibility.TRACKED;
|
|
return (
|
|
<Container>
|
|
<MetaTags title={DOCUMENT_TITLE} description={DOCUMENT_DESCRIPTION} />
|
|
<DocumentTitle title={DOCUMENT_TITLE} />
|
|
<TopBar
|
|
userAddress={this.props.userAddress}
|
|
networkId={this.props.networkId}
|
|
injectedProviderName={this.props.injectedProviderName}
|
|
onToggleLedgerDialog={this._onToggleLedgerDialog.bind(this)}
|
|
dispatcher={this.props.dispatcher}
|
|
providerType={this.props.providerType}
|
|
blockchainIsLoaded={this.props.blockchainIsLoaded}
|
|
location={this.props.location}
|
|
blockchain={this._blockchain}
|
|
translate={this.props.translate}
|
|
displayType={TopBarDisplayType.Expanded}
|
|
style={{
|
|
backgroundColor: colors.lightestGrey,
|
|
position: 'fixed',
|
|
zIndex: zIndex.topBar,
|
|
}}
|
|
maxWidth={LARGE_LAYOUT_MAX_WIDTH}
|
|
/>
|
|
<Container marginTop={TOP_BAR_HEIGHT} minHeight="100vh" backgroundColor={colors.lightestGrey}>
|
|
<Switch>
|
|
<Route path={`${WebsitePaths.Portal}/:route`} render={this._renderOtherRoutes.bind(this)} />
|
|
<Route
|
|
exact={true}
|
|
path={`${WebsitePaths.Portal}/`}
|
|
render={this._renderMainRoute.bind(this)}
|
|
/>
|
|
</Switch>
|
|
<BlockchainErrDialog
|
|
blockchain={this._blockchain}
|
|
blockchainErr={this.props.blockchainErr}
|
|
isOpen={this.props.shouldBlockchainErrDialogBeOpen}
|
|
userAddress={this.props.userAddress}
|
|
toggleDialogFn={updateShouldBlockchainErrDialogBeOpen}
|
|
networkId={this.props.networkId}
|
|
/>
|
|
<FlashMessage dispatcher={this.props.dispatcher} flashMessage={this.props.flashMessage} />
|
|
|
|
<LedgerConfigDialog
|
|
providerType={this.props.providerType}
|
|
networkId={this.props.networkId}
|
|
blockchain={this._blockchain}
|
|
dispatcher={this.props.dispatcher}
|
|
toggleDialogFn={this._onToggleLedgerDialog.bind(this)}
|
|
isOpen={this.state.isLedgerDialogOpen}
|
|
/>
|
|
|
|
<AssetPicker
|
|
userAddress={this.props.userAddress}
|
|
networkId={this.props.networkId}
|
|
blockchain={this._blockchain}
|
|
dispatcher={this.props.dispatcher}
|
|
isOpen={isAssetPickerDialogOpen}
|
|
currentTokenAddress={''}
|
|
onTokenChosen={this._onTokenChosen.bind(this)}
|
|
tokenByAddress={this.props.tokenByAddress}
|
|
tokenVisibility={tokenVisibility}
|
|
/>
|
|
</Container>
|
|
</Container>
|
|
);
|
|
}
|
|
private _renderMainRoute(): React.ReactNode {
|
|
if (this._isSmallScreen()) {
|
|
return <SmallLayout content={this._renderRelayerIndexSection()} />;
|
|
} else {
|
|
return <LargeLayout left={this._renderWalletSection()} right={this._renderRelayerIndexSection()} />;
|
|
}
|
|
}
|
|
private _renderOtherRoutes(routeComponentProps: RouteComponentProps<any>): React.ReactNode {
|
|
if (this._isSmallScreen()) {
|
|
return <SmallLayout content={this._renderAccountManagement()} />;
|
|
} else {
|
|
return <LargeLayout left={this._renderMenu(routeComponentProps)} right={this._renderAccountManagement()} />;
|
|
}
|
|
}
|
|
private _renderMenu(routeComponentProps: RouteComponentProps<any>): React.ReactNode {
|
|
const menuTheme: MenuTheme = {
|
|
paddingLeft: MENU_PADDING_LEFT,
|
|
textColor: colors.darkerGrey,
|
|
iconColor: colors.darkerGrey,
|
|
selectedIconColor: colors.yellow800,
|
|
selectedBackgroundColor: 'transparent',
|
|
};
|
|
return (
|
|
<Section
|
|
header={<BackButton to={WebsitePaths.Portal} labelText="back to Relayers" />}
|
|
body={<Menu selectedPath={routeComponentProps.location.pathname} theme={menuTheme} />}
|
|
/>
|
|
);
|
|
}
|
|
private _renderWallet(): React.ReactNode {
|
|
const isMobile = utils.isMobileWidth(this.props.screenWidth);
|
|
// We need room to scroll down for mobile onboarding
|
|
const marginBottom = isMobile ? '250px' : '15px';
|
|
return (
|
|
<div>
|
|
<Container className="flex flex-column items-center">
|
|
{isMobile && (
|
|
<Container marginTop="20px" marginBottom="20px">
|
|
{this._renderStartOnboarding()}
|
|
</Container>
|
|
)}
|
|
<Container marginBottom={marginBottom} width="100%">
|
|
<Wallet
|
|
style={
|
|
!isMobile && this.props.isPortalOnboardingShowing
|
|
? { zIndex: zIndex.aboveOverlay, position: 'relative' }
|
|
: undefined
|
|
}
|
|
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={this._getCurrentTrackedTokens()}
|
|
userEtherBalanceInWei={this.props.userEtherBalanceInWei}
|
|
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
|
|
injectedProviderName={this.props.injectedProviderName}
|
|
providerType={this.props.providerType}
|
|
screenWidth={this.props.screenWidth}
|
|
location={this.props.location}
|
|
trackedTokenStateByAddress={this.state.trackedTokenStateByAddress}
|
|
onToggleLedgerDialog={this._onToggleLedgerDialog.bind(this)}
|
|
onAddToken={this._onAddToken.bind(this)}
|
|
onRemoveToken={this._onRemoveToken.bind(this)}
|
|
refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this)}
|
|
toggleTooltipDirection={
|
|
this.props.isPortalOnboardingShowing ? PointerDirection.Left : PointerDirection.Right
|
|
}
|
|
/>
|
|
</Container>
|
|
{!isMobile && <Container marginTop="8px">{this._renderStartOnboarding()}</Container>}
|
|
</Container>
|
|
<PortalOnboardingFlow
|
|
blockchain={this._blockchain}
|
|
trackedTokenStateByAddress={this.state.trackedTokenStateByAddress}
|
|
refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this)}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
private _renderStartOnboarding(): React.ReactNode {
|
|
const isMobile = utils.isMobileWidth(this.props.screenWidth);
|
|
const shouldStartOnboarding = !isMobile || this.props.location.pathname === `${WebsitePaths.Portal}/account`;
|
|
const startOnboarding = (
|
|
<Container className="flex items-center center">
|
|
<Text fontColor={colors.mediumBlue} fontSize="16px" onClick={this._startOnboarding.bind(this)}>
|
|
Set up your account to start trading
|
|
</Text>
|
|
<Container marginLeft="8px" paddingTop="3px">
|
|
<Image src="/images/setup_account_icon.svg" height="20px" width="20x" />
|
|
</Container>
|
|
</Container>
|
|
);
|
|
return !shouldStartOnboarding ? (
|
|
<Link to={`${WebsitePaths.Portal}/account`}>{startOnboarding}</Link>
|
|
) : (
|
|
startOnboarding
|
|
);
|
|
}
|
|
private _startOnboarding(): void {
|
|
analytics.track('Onboarding Started', {
|
|
reason: 'manual',
|
|
stepIndex: this.props.portalOnboardingStep,
|
|
});
|
|
this.props.dispatcher.updatePortalOnboardingShowing(true);
|
|
}
|
|
private _renderWalletSection(): React.ReactNode {
|
|
return <Section header={<TextHeader labelText="Your Account" />} body={this._renderWallet()} />;
|
|
}
|
|
private _renderAccountManagement(): React.ReactNode {
|
|
const accountManagementItems: AccountManagementItem[] = [
|
|
{
|
|
pathName: `${WebsitePaths.Portal}/weth`,
|
|
headerText: 'Wrapped ETH',
|
|
render: this._renderEthWrapper.bind(this),
|
|
},
|
|
{
|
|
pathName: `${WebsitePaths.Portal}/account`,
|
|
headerText: this._isSmallScreen() ? undefined : 'Your Account',
|
|
render: this._isSmallScreen() ? this._renderWallet.bind(this) : this._renderTokenBalances.bind(this),
|
|
},
|
|
{
|
|
pathName: `${WebsitePaths.Portal}/trades`,
|
|
headerText: 'Trade History',
|
|
render: this._renderTradeHistory.bind(this),
|
|
},
|
|
{
|
|
pathName: `${WebsitePaths.Portal}/generate`,
|
|
headerText: 'Generate Order',
|
|
render: this._renderGenerateOrderForm.bind(this),
|
|
},
|
|
{
|
|
pathName: `${WebsitePaths.Portal}/fill`,
|
|
headerText: 'Fill Order',
|
|
render: this._renderFillOrder.bind(this),
|
|
},
|
|
];
|
|
return (
|
|
<div>
|
|
<Switch>
|
|
{_.map(accountManagementItems, item => {
|
|
return (
|
|
<Route
|
|
key={item.pathName}
|
|
path={item.pathName}
|
|
render={this._renderAccountManagementItem.bind(this, item)}
|
|
/>
|
|
);
|
|
})}}
|
|
<Route render={this._renderNotFoundMessage.bind(this)} />
|
|
</Switch>
|
|
<PortalDisclaimerDialog
|
|
isOpen={this.state.isDisclaimerDialogOpen}
|
|
onToggleDialog={this._onPortalDisclaimerAccepted.bind(this)}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
private _renderAccountManagementItem(item: AccountManagementItem): React.ReactNode {
|
|
return (
|
|
<Section
|
|
header={!_.isUndefined(item.headerText) && <TextHeader labelText={item.headerText} />}
|
|
body={<Loading isLoading={!this.props.blockchainIsLoaded} content={item.render()} />}
|
|
/>
|
|
);
|
|
}
|
|
private _renderEthWrapper(): React.ReactNode {
|
|
return (
|
|
<EthWrappers
|
|
networkId={this.props.networkId}
|
|
blockchain={this._blockchain}
|
|
dispatcher={this.props.dispatcher}
|
|
tokenByAddress={this.props.tokenByAddress}
|
|
userAddress={this.props.userAddress}
|
|
userEtherBalanceInWei={this.props.userEtherBalanceInWei}
|
|
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
|
|
isFullWidth={true}
|
|
/>
|
|
);
|
|
}
|
|
private _renderTradeHistory(): React.ReactNode {
|
|
return (
|
|
<TradeHistory
|
|
tokenByAddress={this.props.tokenByAddress}
|
|
userAddress={this.props.userAddress}
|
|
networkId={this.props.networkId}
|
|
isFullWidth={true}
|
|
shouldHideHeader={true}
|
|
isScrollable={false}
|
|
/>
|
|
);
|
|
}
|
|
private _renderGenerateOrderForm(): React.ReactNode {
|
|
return (
|
|
<GenerateOrderForm
|
|
blockchain={this._blockchain}
|
|
hashData={this.props.hashData}
|
|
dispatcher={this.props.dispatcher}
|
|
isFullWidth={true}
|
|
shouldHideHeader={true}
|
|
/>
|
|
);
|
|
}
|
|
private _renderFillOrder(): React.ReactNode {
|
|
const initialFillOrder = !_.isUndefined(this.props.userSuppliedOrderCache)
|
|
? this.props.userSuppliedOrderCache
|
|
: this._sharedOrderIfExists;
|
|
return (
|
|
<FillOrder
|
|
blockchain={this._blockchain}
|
|
blockchainErr={this.props.blockchainErr}
|
|
initialOrder={initialFillOrder}
|
|
isOrderInUrl={!_.isUndefined(this._sharedOrderIfExists)}
|
|
orderFillAmount={this.props.orderFillAmount}
|
|
networkId={this.props.networkId}
|
|
userAddress={this.props.userAddress}
|
|
tokenByAddress={this.props.tokenByAddress}
|
|
dispatcher={this.props.dispatcher}
|
|
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
|
|
isFullWidth={true}
|
|
shouldHideHeader={true}
|
|
/>
|
|
);
|
|
}
|
|
private _renderTokenBalances(): React.ReactNode {
|
|
return (
|
|
<TokenBalances
|
|
blockchain={this._blockchain}
|
|
blockchainErr={this.props.blockchainErr}
|
|
blockchainIsLoaded={this.props.blockchainIsLoaded}
|
|
dispatcher={this.props.dispatcher}
|
|
screenWidth={this.props.screenWidth}
|
|
tokenByAddress={this.props.tokenByAddress}
|
|
trackedTokens={this._getCurrentTrackedTokens()}
|
|
userAddress={this.props.userAddress}
|
|
userEtherBalanceInWei={this.props.userEtherBalanceInWei}
|
|
networkId={this.props.networkId}
|
|
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
|
|
isFullWidth={true}
|
|
/>
|
|
);
|
|
}
|
|
private _renderRelayerIndexSection(): React.ReactNode {
|
|
const isMobile = utils.isMobileWidth(this.props.screenWidth);
|
|
// TODO(bmillman): revert RelayerIndex cellStyle to Expanded once data pipeline is tracking v2 volume
|
|
return (
|
|
<Section
|
|
header={!isMobile && <TextHeader labelText="0x Relayers" />}
|
|
body={
|
|
<Container className="flex flex-column">
|
|
{isMobile && (
|
|
<Container marginTop="20px" marginBottom="20px">
|
|
{this._renderStartOnboarding()}
|
|
</Container>
|
|
)}
|
|
<RelayerIndex
|
|
networkId={this.props.networkId}
|
|
screenWidth={this.props.screenWidth}
|
|
cellStyle={RelayerIndexCellStyle.Minimized}
|
|
/>
|
|
</Container>
|
|
}
|
|
/>
|
|
);
|
|
}
|
|
private _renderNotFoundMessage(): React.ReactNode {
|
|
return (
|
|
<FullscreenMessage
|
|
headerText="404 Not Found"
|
|
bodyText="Hm... looks like we couldn't find what you are looking for."
|
|
/>
|
|
);
|
|
}
|
|
private _onTokenChosen(tokenAddress: string): void {
|
|
if (_.isEmpty(tokenAddress)) {
|
|
this.setState({
|
|
tokenManagementState: TokenManagementState.None,
|
|
});
|
|
return;
|
|
}
|
|
const token = this.props.tokenByAddress[tokenAddress];
|
|
const isDefaultTrackedToken = _.includes(configs.DEFAULT_TRACKED_TOKEN_SYMBOLS, token.symbol);
|
|
if (this.state.tokenManagementState === TokenManagementState.Remove && !isDefaultTrackedToken) {
|
|
if (token.isRegistered) {
|
|
// Remove the token from tracked tokens
|
|
const newToken: Token = {
|
|
...token,
|
|
trackedTimestamp: undefined,
|
|
};
|
|
this.props.dispatcher.updateTokenByAddress([newToken]);
|
|
} else {
|
|
this.props.dispatcher.removeTokenToTokenByAddress(token);
|
|
}
|
|
trackedTokenStorage.removeTrackedToken(this.props.userAddress, this.props.networkId, tokenAddress);
|
|
} else if (isDefaultTrackedToken) {
|
|
this.props.dispatcher.showFlashMessage(`Cannot remove ${token.name} because it's a default token`);
|
|
}
|
|
this.setState({
|
|
tokenManagementState: TokenManagementState.None,
|
|
});
|
|
}
|
|
private _onToggleLedgerDialog(): void {
|
|
this.setState({
|
|
isLedgerDialogOpen: !this.state.isLedgerDialogOpen,
|
|
});
|
|
}
|
|
private _onAddToken(): void {
|
|
this.setState({
|
|
tokenManagementState: TokenManagementState.Add,
|
|
});
|
|
}
|
|
private _onRemoveToken(): void {
|
|
this.setState({
|
|
tokenManagementState: TokenManagementState.Remove,
|
|
});
|
|
}
|
|
private _onPortalDisclaimerAccepted(): void {
|
|
localStorage.setItem(constants.LOCAL_STORAGE_KEY_ACCEPT_DISCLAIMER, 'set');
|
|
this.setState({
|
|
isDisclaimerDialogOpen: false,
|
|
});
|
|
}
|
|
private _updateScreenWidth(): void {
|
|
const newScreenWidth = utils.getScreenWidth();
|
|
this.props.dispatcher.updateScreenWidth(newScreenWidth);
|
|
}
|
|
private _isSmallScreen(): boolean {
|
|
const isSmallScreen = this.props.screenWidth === ScreenWidths.Sm;
|
|
return isSmallScreen;
|
|
}
|
|
private _getCurrentTrackedTokens(): Token[] {
|
|
return utils.getTrackedTokens(this.props.tokenByAddress);
|
|
}
|
|
private _getCurrentTrackedTokensAddresses(): string[] {
|
|
return _.map(this._getCurrentTrackedTokens(), token => token.address);
|
|
}
|
|
private _getInitialTrackedTokenStateByAddress(trackedTokens: Token[]): TokenStateByAddress {
|
|
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[]): Promise<void> {
|
|
const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress;
|
|
const userAddressIfExists = _.isEmpty(this.props.userAddress) ? undefined : this.props.userAddress;
|
|
const balancesAndAllowances = await Promise.all(
|
|
tokenAddresses.map(async tokenAddress => {
|
|
return this._blockchain.getTokenBalanceAndAllowanceAsync(userAddressIfExists, tokenAddress);
|
|
}),
|
|
);
|
|
const priceByAddress = await this._getPriceByAddressAsync(tokenAddresses);
|
|
for (let i = 0; i < tokenAddresses.length; i++) {
|
|
// Order is preserved in Promise.all
|
|
const [balance, allowance] = balancesAndAllowances[i];
|
|
const tokenAddress = tokenAddresses[i];
|
|
trackedTokenStateByAddress[tokenAddress] = {
|
|
balance,
|
|
allowance,
|
|
isLoaded: true,
|
|
price: priceByAddress[tokenAddress],
|
|
};
|
|
}
|
|
this.setState({
|
|
trackedTokenStateByAddress,
|
|
});
|
|
}
|
|
|
|
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 async _refetchTokenStateAsync(tokenAddress: string): Promise<void> {
|
|
await this._fetchBalancesAndAllowancesAsync([tokenAddress]);
|
|
}
|
|
}
|
|
|
|
interface LargeLayoutProps {
|
|
left: React.ReactNode;
|
|
right: React.ReactNode;
|
|
}
|
|
const LargeLayout = (props: LargeLayoutProps) => {
|
|
return (
|
|
<Container
|
|
className="mx-auto flex flex-center"
|
|
maxWidth={LARGE_LAYOUT_MAX_WIDTH}
|
|
paddingLeft={SIDE_PADDING}
|
|
paddingRight={SIDE_PADDING}
|
|
>
|
|
<div className="flex-last">
|
|
<Container width={LEFT_COLUMN_WIDTH} position="fixed" zIndex={zIndex.aboveTopBar}>
|
|
{props.left}
|
|
</Container>
|
|
</div>
|
|
<Container className="flex-auto" marginLeft={LEFT_COLUMN_WIDTH}>
|
|
<Container className="flex-auto" marginLeft={SIDE_PADDING}>
|
|
{props.right}
|
|
</Container>
|
|
</Container>
|
|
</Container>
|
|
);
|
|
};
|
|
|
|
interface SmallLayoutProps {
|
|
content: React.ReactNode;
|
|
}
|
|
const SmallLayout = (props: SmallLayoutProps) => {
|
|
return (
|
|
<div className="flex flex-center">
|
|
<Container className="flex-auto" paddingLeft={SIDE_PADDING} paddingRight={SIDE_PADDING}>
|
|
{props.content}
|
|
</Container>
|
|
</div>
|
|
);
|
|
}; // tslint:disable:max-file-line-count
|