diff options
Diffstat (limited to 'packages/website/ts')
5 files changed, 127 insertions, 97 deletions
diff --git a/packages/website/ts/blockchain.ts b/packages/website/ts/blockchain.ts index 212e829dd..6e4d03e27 100644 --- a/packages/website/ts/blockchain.ts +++ b/packages/website/ts/blockchain.ts @@ -506,7 +506,7 @@ export class Blockchain { public async getTokenBalanceAndAllowanceAsync( ownerAddressIfExists: string, tokenAddress: string, - ): Promise<BigNumber[]> { + ): Promise<[BigNumber, BigNumber]> { utils.assert(!_.isUndefined(this._contractWrappers), 'ContractWrappers must be instantiated.'); if (_.isUndefined(ownerAddressIfExists)) { @@ -516,8 +516,10 @@ export class Blockchain { let balance = new BigNumber(0); let allowance = new BigNumber(0); if (this._doesUserAddressExist()) { - balance = await this._contractWrappers.token.getBalanceAsync(tokenAddress, ownerAddressIfExists); - allowance = await this._contractWrappers.token.getProxyAllowanceAsync(tokenAddress, ownerAddressIfExists); + [balance, allowance] = await Promise.all([ + this._contractWrappers.token.getBalanceAsync(tokenAddress, ownerAddressIfExists), + this._contractWrappers.token.getProxyAllowanceAsync(tokenAddress, ownerAddressIfExists), + ]); } return [balance, allowance]; } diff --git a/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx b/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx index 2014dd7b0..bf52684d7 100644 --- a/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx +++ b/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { BigNumber } from '@0xproject/utils'; import { OnboardingFlow, Step } from 'ts/components/onboarding/onboarding_flow'; -import { ProviderType, TokenByAddress } from 'ts/types'; +import { ProviderType, TokenByAddress, TokenStateByAddress } from 'ts/types'; import { utils } from 'ts/utils/utils'; export interface PortalOnboardingFlowProps { @@ -16,6 +16,7 @@ export interface PortalOnboardingFlowProps { blockchainIsLoaded: boolean; userEtherBalanceInWei?: BigNumber; tokenByAddress: TokenByAddress; + trackedTokenStateByAddress: TokenStateByAddress; updateIsRunning: (isRunning: boolean) => void; updateOnboardingStep: (stepIndex: number) => void; } @@ -68,13 +69,13 @@ export class PortalOnboardingFlow extends React.Component<PortalOnboardingFlowPr target: '.eth-row', content: 'Before you begin you will need to send some ETH to your metamask wallet', placement: 'right', - continueButtonDisplay: this._userHasEth() ? 'enabled' : 'disabled', + continueButtonDisplay: this._userHasVisibleEth() ? 'enabled' : 'disabled', }, { target: '.weth-row', content: 'You need to convert some of your ETH into tradeable Wrapped ETH (WETH)', placement: 'right', - continueButtonDisplay: this._userHasWeth() ? 'enabled' : 'disabled', + continueButtonDisplay: this._userHasVisibleWeth() ? 'enabled' : 'disabled', }, ]; return steps; @@ -84,13 +85,17 @@ export class PortalOnboardingFlow extends React.Component<PortalOnboardingFlowPr return !_.isEmpty(this.props.userAddress); } - private _userHasEth(): boolean { + private _userHasVisibleEth(): boolean { return this.props.userEtherBalanceInWei > new BigNumber(0); } - private _userHasWeth(): boolean { - // TODO: https://app.asana.com/0/681385331277907/690722374136933 - return false; + private _userHasVisibleWeth(): boolean { + const ethToken = utils.getEthToken(this.props.tokenByAddress); + if (!ethToken) { + return false; + } + const wethTokenState = this.props.trackedTokenStateByAddress[ethToken.address]; + return wethTokenState.balance > new BigNumber(0); } private _overrideOnboardingStateIfShould(): void { diff --git a/packages/website/ts/components/portal/portal.tsx b/packages/website/ts/components/portal/portal.tsx index 589ad00ad..9aa83546a 100644 --- a/packages/website/ts/components/portal/portal.tsx +++ b/packages/website/ts/components/portal/portal.tsx @@ -37,7 +37,9 @@ import { Order, ProviderType, ScreenWidths, + Token, TokenByAddress, + TokenStateByAddress, TokenVisibility, WebsitePaths, } from 'ts/types'; @@ -77,6 +79,7 @@ interface PortalState { isDisclaimerDialogOpen: boolean; isLedgerDialogOpen: boolean; tokenManagementState: TokenManagementState; + trackedTokenStateByAddress: TokenStateByAddress; } interface AccountManagementItem { @@ -127,6 +130,9 @@ export class Portal extends React.Component<PortalProps, PortalState> { 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, @@ -135,6 +141,7 @@ export class Portal extends React.Component<PortalProps, PortalState> { isDisclaimerDialogOpen: !hasAcceptedDisclaimer, tokenManagementState: TokenManagementState.None, isLedgerDialogOpen: false, + trackedTokenStateByAddress: initialTrackedTokenStateByAddress, }; } public componentDidMount(): void { @@ -143,6 +150,9 @@ export class Portal extends React.Component<PortalProps, PortalState> { } public componentWillMount(): void { this._blockchain = new Blockchain(this.props.dispatcher); + const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress); + // tslint:disable-next-line:no-floating-promises + this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses); } public componentWillUnmount(): void { this._blockchain.destroy(); @@ -178,6 +188,39 @@ export class Portal extends React.Component<PortalProps, PortalState> { prevPathname: nextProps.location.pathname, }); } + 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); + } + + const nextTrackedTokens = this._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( @@ -190,7 +233,7 @@ export class Portal extends React.Component<PortalProps, PortalState> { : TokenVisibility.TRACKED; return ( <div style={styles.root}> - <PortalOnboardingFlow /> + <PortalOnboardingFlow trackedTokenStateByAddress={this.state.trackedTokenStateByAddress} /> <DocumentTitle title="0x Portal DApp" /> <TopBar userAddress={this.props.userAddress} @@ -283,8 +326,6 @@ export class Portal extends React.Component<PortalProps, PortalState> { ); } private _renderWallet(): React.ReactNode { - const allTokens = _.values(this.props.tokenByAddress); - const trackedTokens = _.filter(allTokens, t => t.isTracked); return ( <div> <Wallet @@ -295,16 +336,18 @@ export class Portal extends React.Component<PortalProps, PortalState> { blockchainErr={this.props.blockchainErr} dispatcher={this.props.dispatcher} tokenByAddress={this.props.tokenByAddress} - trackedTokens={trackedTokens} + 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)} /> <Container marginTop="15px"> <Island> @@ -424,8 +467,6 @@ export class Portal extends React.Component<PortalProps, PortalState> { ); } private _renderTokenBalances(): React.ReactNode { - const allTokens = _.values(this.props.tokenByAddress); - const trackedTokens = _.filter(allTokens, t => t.isTracked); return ( <TokenBalances blockchain={this._blockchain} @@ -434,7 +475,7 @@ export class Portal extends React.Component<PortalProps, PortalState> { dispatcher={this.props.dispatcher} screenWidth={this.props.screenWidth} tokenByAddress={this.props.tokenByAddress} - trackedTokens={trackedTokens} + trackedTokens={this._getCurrentTrackedTokens()} userAddress={this.props.userAddress} userEtherBalanceInWei={this.props.userEtherBalanceInWei} networkId={this.props.networkId} @@ -515,6 +556,55 @@ export class Portal extends React.Component<PortalProps, PortalState> { const isSmallScreen = this.props.screenWidth === ScreenWidths.Sm; return isSmallScreen; } + + private _getCurrentTrackedTokens(): Token[] { + return this._getTrackedTokens(this.props.tokenByAddress); + } + + private _getTrackedTokens(tokenByAddress: TokenByAddress): Token[] { + const allTokens = _.values(tokenByAddress); + const trackedTokens = _.filter(allTokens, t => t.isTracked); + return trackedTokens; + } + + 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); + }), + ); + 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, + }; + } + this.setState({ + trackedTokenStateByAddress, + }); + } + + private async _refetchTokenStateAsync(tokenAddress: string): Promise<void> { + await this._fetchBalancesAndAllowancesAsync([tokenAddress]); + } } interface LargeLayoutProps { diff --git a/packages/website/ts/components/wallet/wallet.tsx b/packages/website/ts/components/wallet/wallet.tsx index bff4aadff..4383179a1 100644 --- a/packages/website/ts/components/wallet/wallet.tsx +++ b/packages/website/ts/components/wallet/wallet.tsx @@ -67,13 +67,14 @@ export interface WalletProps { providerType: ProviderType; screenWidth: ScreenWidths; location: Location; + trackedTokenStateByAddress: TokenStateByAddress; onToggleLedgerDialog: () => void; onAddToken: () => void; onRemoveToken: () => void; + refetchTokenStateAsync: (tokenAddress: string) => Promise<void>; } interface WalletState { - trackedTokenStateByAddress: TokenStateByAddress; wrappedEtherDirection?: Side; isHoveringSidebar: boolean; } @@ -151,48 +152,12 @@ export class Wallet extends React.Component<WalletProps, WalletState> { 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 isBlockchainLoaded = this.props.blockchainIsLoaded && this.props.blockchainErr === BlockchainErrs.NoError; return ( @@ -342,7 +307,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> { _.isUndefined(this.props.userEtherBalanceInWei), ); const etherToken = this._getEthToken(); - const etherTokenState = this.state.trackedTokenStateByAddress[etherToken.address]; + const etherTokenState = this.props.trackedTokenStateByAddress[etherToken.address]; const etherPrice = etherTokenState.price; const secondaryText = this._renderValue( this.props.userEtherBalanceInWei || new BigNumber(0), @@ -366,7 +331,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> { return _.map(trackedTokensStartingWithEtherToken, this._renderTokenRow.bind(this)); } private _renderTokenRow(token: Token, index: number): React.ReactNode { - const tokenState = this.state.trackedTokenStateByAddress[token.address]; + const tokenState = this.props.trackedTokenStateByAddress[token.address]; const tokenLink = sharedUtils.getEtherScanLinkIfExists( token.address, this.props.networkId, @@ -438,7 +403,8 @@ export class Wallet extends React.Component<WalletProps, WalletState> { etherToken={etherToken} lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} onConversionSuccessful={this._closeWrappedEtherActionRow.bind(this)} - refetchEthTokenStateAsync={this._refetchTokenStateAsync.bind(this, etherToken.address)} + // tslint:disable:jsx-no-lambda + refetchEthTokenStateAsync={async () => this.props.refetchTokenStateAsync(etherToken.address)} /> )} </div> @@ -474,7 +440,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> { onErrorOccurred={_.noop} // TODO: Error handling userAddress={this.props.userAddress} isDisabled={!config.tokenState.isLoaded} - refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this, config.token.address)} + refetchTokenStateAsync={async () => this.props.refetchTokenStateAsync(config.token.address)} /> ); } @@ -557,42 +523,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> { }); 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 {}; diff --git a/packages/website/ts/containers/portal_onboarding_flow.ts b/packages/website/ts/containers/portal_onboarding_flow.ts index 8202fb2ae..0ad9aef13 100644 --- a/packages/website/ts/containers/portal_onboarding_flow.ts +++ b/packages/website/ts/containers/portal_onboarding_flow.ts @@ -2,12 +2,14 @@ import { BigNumber } from '@0xproject/utils'; import * as React from 'react'; import { connect } from 'react-redux'; import { Dispatch } from 'redux'; -import { ActionTypes, ProviderType, TokenByAddress } from 'ts/types'; +import { ActionTypes, ProviderType, TokenByAddress, TokenStateByAddress } from 'ts/types'; import { PortalOnboardingFlow as PortalOnboardingFlowComponent } from 'ts/components/onboarding/portal_onboarding_flow'; import { State } from 'ts/redux/reducer'; -interface PortalOnboardingFlowProps {} +interface PortalOnboardingFlowProps { + trackedTokenStateByAddress: TokenStateByAddress; +} interface ConnectedState { stepIndex: number; @@ -26,7 +28,7 @@ interface ConnectedDispatch { updateOnboardingStep: (stepIndex: number) => void; } -const mapStateToProps = (state: State): ConnectedState => ({ +const mapStateToProps = (state: State, ownProps: PortalOnboardingFlowProps): ConnectedState => ({ stepIndex: state.portalOnboardingStep, isRunning: state.isPortalOnboardingShowing, userAddress: state.userAddress, |