From 6206ebc994a2cf76b90ac426218d6ed18b74a072 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Sun, 28 Jan 2018 16:19:55 +0100 Subject: Implement just-in-time loading of token balances & allowances --- packages/website/ts/blockchain.ts | 59 +++------ .../dialogs/eth_weth_conversion_dialog.tsx | 60 ++++++--- .../ts/components/dialogs/ledger_config_dialog.tsx | 3 +- .../website/ts/components/dialogs/send_dialog.tsx | 13 +- .../dialogs/track_token_confirmation_dialog.tsx | 10 -- .../ts/components/eth_weth_conversion_button.tsx | 14 +- packages/website/ts/components/eth_wrappers.tsx | 86 ++++++++++-- packages/website/ts/components/fill_order.tsx | 17 ++- .../ts/components/generate_order/asset_picker.tsx | 14 +- .../generate_order/generate_order_form.tsx | 20 ++- .../components/generate_order/new_token_form.tsx | 14 +- .../ts/components/inputs/allowance_toggle.tsx | 5 +- .../ts/components/inputs/balance_bounded_input.tsx | 3 + .../ts/components/inputs/token_amount_input.tsx | 54 +++++++- packages/website/ts/components/portal.tsx | 22 +--- packages/website/ts/components/send_button.tsx | 13 +- packages/website/ts/components/token_balances.tsx | 146 ++++++++++++++++----- .../ts/components/top_bar/provider_picker.tsx | 2 +- .../website/ts/containers/generate_order_form.tsx | 4 +- packages/website/ts/containers/portal.tsx | 4 +- packages/website/ts/redux/dispatcher.ts | 38 +----- packages/website/ts/redux/reducer.ts | 72 +--------- packages/website/ts/types.ts | 6 +- 23 files changed, 384 insertions(+), 295 deletions(-) diff --git a/packages/website/ts/blockchain.ts b/packages/website/ts/blockchain.ts index 6fc56aecd..898cb2d01 100644 --- a/packages/website/ts/blockchain.ts +++ b/packages/website/ts/blockchain.ts @@ -66,7 +66,6 @@ export class Blockchain { private _cachedProvider: Web3.Provider; private _cachedProviderNetworkId: number; private _ledgerSubprovider: LedgerWalletSubprovider; - private _zrxPollIntervalId: NodeJS.Timer; private static async _onPageLoadAsync(): Promise { if (document.readyState === 'complete') { return; // Already loaded @@ -250,7 +249,6 @@ export class Blockchain { ); await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash); const allowance = amountInBaseUnits; - this._dispatcher.replaceTokenAllowanceByAddress(token.address, allowance); } public async transferAsync(token: Token, toAddress: string, amountInBaseUnits: BigNumber): Promise { const txHash = await this._zeroEx.token.transferAsync( @@ -368,22 +366,25 @@ export class Blockchain { const [currBalance] = await this.getTokenBalanceAndAllowanceAsync(this._userAddress, token.address); - this._zrxPollIntervalId = intervalUtils.setAsyncExcludingInterval( - async () => { - const [balance] = await this.getTokenBalanceAndAllowanceAsync(this._userAddress, token.address); - if (!balance.eq(currBalance)) { - this._dispatcher.replaceTokenBalanceByAddress(token.address, balance); - intervalUtils.clearAsyncExcludingInterval(this._zrxPollIntervalId); - delete this._zrxPollIntervalId; - } - }, - 5000, - (err: Error) => { - utils.consoleLog(`Polling tokenBalance failed: ${err}`); - intervalUtils.clearAsyncExcludingInterval(this._zrxPollIntervalId); - delete this._zrxPollIntervalId; - }, - ); + const newTokenBalancePromise = new Promise((resolve: (balance: BigNumber) => void, reject) => { + const tokenPollInterval = intervalUtils.setAsyncExcludingInterval( + async () => { + const [balance] = await this.getTokenBalanceAndAllowanceAsync(this._userAddress, token.address); + if (!balance.eq(currBalance)) { + intervalUtils.clearAsyncExcludingInterval(tokenPollInterval); + resolve(balance); + } + }, + 5000, + (err: Error) => { + utils.consoleLog(`Polling tokenBalance failed: ${err}`); + intervalUtils.clearAsyncExcludingInterval(tokenPollInterval); + reject(err); + }, + ); + }); + + return newTokenBalancePromise; } public async signOrderHashAsync(orderHash: string): Promise { utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.'); @@ -408,7 +409,6 @@ export class Blockchain { from: this._userAddress, }); const balanceDelta = constants.MINT_AMOUNT; - this._dispatcher.updateTokenBalanceByAddress(token.address, balanceDelta); } public async getBalanceInEthAsync(owner: string): Promise { const balance = await this._web3Wrapper.getBalanceInEthAsync(owner); @@ -451,23 +451,6 @@ export class Blockchain { } return [balance, allowance]; } - public async updateTokenBalancesAndAllowancesAsync(tokens: Token[]) { - const err = new Error('show stopper'); - const tokenStateByAddress: TokenStateByAddress = {}; - for (const token of tokens) { - let balance = new BigNumber(0); - let allowance = new BigNumber(0); - if (this._doesUserAddressExist()) { - [balance, allowance] = await this.getTokenBalanceAndAllowanceAsync(this._userAddress, token.address); - } - const tokenState = { - balance, - allowance, - }; - tokenStateByAddress[token.address] = tokenState; - } - this._dispatcher.updateTokenStateByAddress(tokenStateByAddress); - } public async getUserAccountsAsync() { utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.'); const userAccountsIfExists = await this._zeroEx.getAvailableAddressesAsync(); @@ -480,7 +463,6 @@ export class Blockchain { this._web3Wrapper.updatePrevUserAddress(newUserAddress); } public destroy() { - intervalUtils.clearAsyncExcludingInterval(this._zrxPollIntervalId); this._web3Wrapper.destroy(); this._stopWatchingExchangeLogFillEvents(); } @@ -527,9 +509,6 @@ export class Blockchain { const allTokens = _.uniq([...tokenRegistryTokens, ...trackedTokensIfExists]); this._dispatcher.updateTokenByAddress(allTokens); - // Get balance/allowance for tracked tokens - await this.updateTokenBalancesAndAllowancesAsync(trackedTokensIfExists); - const mostPopularTradingPairTokens: Token[] = [ _.find(allTokens, { symbol: configs.DEFAULT_TRACKED_TOKEN_SYMBOLS[0] }), _.find(allTokens, { symbol: configs.DEFAULT_TRACKED_TOKEN_SYMBOLS[1] }), diff --git a/packages/website/ts/components/dialogs/eth_weth_conversion_dialog.tsx b/packages/website/ts/components/dialogs/eth_weth_conversion_dialog.tsx index 661cc1d8c..a3a39a1b9 100644 --- a/packages/website/ts/components/dialogs/eth_weth_conversion_dialog.tsx +++ b/packages/website/ts/components/dialogs/eth_weth_conversion_dialog.tsx @@ -2,25 +2,31 @@ import { BigNumber } from '@0xproject/utils'; import Dialog from 'material-ui/Dialog'; import FlatButton from 'material-ui/FlatButton'; import * as React from 'react'; +import { Blockchain } from 'ts/blockchain'; import { EthAmountInput } from 'ts/components/inputs/eth_amount_input'; import { TokenAmountInput } from 'ts/components/inputs/token_amount_input'; -import { Side, Token, TokenState } from 'ts/types'; +import { Side, Token } from 'ts/types'; import { colors } from 'ts/utils/colors'; interface EthWethConversionDialogProps { + blockchain: Blockchain; + userAddress: string; + networkId: number; direction: Side; onComplete: (direction: Side, value: BigNumber) => void; onCancelled: () => void; isOpen: boolean; token: Token; - tokenState: TokenState; etherBalance: BigNumber; + lastForceTokenStateRefetch: number; } interface EthWethConversionDialogState { value?: BigNumber; shouldShowIncompleteErrs: boolean; hasErrors: boolean; + isEthTokenBalanceLoaded: boolean; + ethTokenBalance: BigNumber; } export class EthWethConversionDialog extends React.Component< @@ -32,8 +38,14 @@ export class EthWethConversionDialog extends React.Component< this.state = { shouldShowIncompleteErrs: false, hasErrors: false, + isEthTokenBalanceLoaded: false, + ethTokenBalance: new BigNumber(0), }; } + public componentWillMount() { + // tslint:disable-next-line:no-floating-promises + this._fetchEthTokenBalanceAsync(); + } public render() { const convertDialogActions = [ , @@ -72,8 +84,11 @@ export class EthWethConversionDialog extends React.Component<
{this.props.direction === Side.Receive ? (
1 ETH = 1 WETH
- {this.props.direction === Side.Receive && ( -
- Max -
- )} + {this.props.direction === Side.Receive && + this.state.isEthTokenBalanceLoaded && ( +
+ Max +
+ )}
@@ -132,7 +148,7 @@ export class EthWethConversionDialog extends React.Component< } private _onMaxClick() { this.setState({ - value: this.props.tokenState.balance, + value: this.state.ethTokenBalance, }); } private _onValueChange(isValid: boolean, amount?: BigNumber) { @@ -160,4 +176,14 @@ export class EthWethConversionDialog extends React.Component< }); this.props.onCancelled(); } + private async _fetchEthTokenBalanceAsync() { + const [balance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync( + this.props.userAddress, + this.props.token.address, + ); + this.setState({ + isEthTokenBalanceLoaded: true, + ethTokenBalance: balance, + }); + } } diff --git a/packages/website/ts/components/dialogs/ledger_config_dialog.tsx b/packages/website/ts/components/dialogs/ledger_config_dialog.tsx index aff3f67b1..a17a51622 100644 --- a/packages/website/ts/components/dialogs/ledger_config_dialog.tsx +++ b/packages/website/ts/components/dialogs/ledger_config_dialog.tsx @@ -197,7 +197,8 @@ export class LedgerConfigDialog extends React.Component void; onCancelled: () => void; isOpen: boolean; token: Token; - tokenState: TokenState; + lastForceTokenStateRefetch: number; } interface SendDialogState { @@ -66,15 +70,18 @@ export class SendDialog extends React.Component ); diff --git a/packages/website/ts/components/dialogs/track_token_confirmation_dialog.tsx b/packages/website/ts/components/dialogs/track_token_confirmation_dialog.tsx index 3f29d46f8..bb7e3ed1a 100644 --- a/packages/website/ts/components/dialogs/track_token_confirmation_dialog.tsx +++ b/packages/website/ts/components/dialogs/track_token_confirmation_dialog.tsx @@ -82,16 +82,6 @@ export class TrackTokenConfirmationDialog extends React.Component< newTokenEntry.isTracked = true; trackedTokenStorage.addTrackedTokenToUser(this.props.userAddress, this.props.networkId, newTokenEntry); this.props.dispatcher.updateTokenByAddress([newTokenEntry]); - - const [balance, allowance] = await this.props.blockchain.getCurrentUserTokenBalanceAndAllowanceAsync( - token.address, - ); - this.props.dispatcher.updateTokenStateByAddress({ - [token.address]: { - balance, - allowance, - }, - }); } this.setState({ diff --git a/packages/website/ts/components/eth_weth_conversion_button.tsx b/packages/website/ts/components/eth_weth_conversion_button.tsx index cc5e623ea..52240fd0f 100644 --- a/packages/website/ts/components/eth_weth_conversion_button.tsx +++ b/packages/website/ts/components/eth_weth_conversion_button.tsx @@ -12,6 +12,8 @@ import { errorReporter } from 'ts/utils/error_reporter'; import { utils } from 'ts/utils/utils'; interface EthWethConversionButtonProps { + userAddress: string; + networkId: number; direction: Side; ethToken: Token; ethTokenState: TokenState; @@ -21,6 +23,8 @@ interface EthWethConversionButtonProps { isOutdatedWrappedEther: boolean; onConversionSuccessful?: () => void; isDisabled?: boolean; + lastForceTokenStateRefetch: number; + refetchEthTokenStateAsync: () => Promise; } interface EthWethConversionButtonState { @@ -64,13 +68,16 @@ export class EthWethConversionButton extends React.Component< onClick={this._toggleConversionDialog.bind(this)} /> ); @@ -87,21 +94,18 @@ export class EthWethConversionButton extends React.Component< this._toggleConversionDialog(); const token = this.props.ethToken; const tokenState = this.props.ethTokenState; - let balance = tokenState.balance; try { if (direction === Side.Deposit) { await this.props.blockchain.convertEthToWrappedEthTokensAsync(token.address, value); const ethAmount = ZeroEx.toUnitAmount(value, constants.DECIMAL_PLACES_ETH); this.props.dispatcher.showFlashMessage(`Successfully wrapped ${ethAmount.toString()} ETH to WETH`); - balance = balance.plus(value); } else { await this.props.blockchain.convertWrappedEthTokensToEthAsync(token.address, value); const tokenAmount = ZeroEx.toUnitAmount(value, token.decimals); this.props.dispatcher.showFlashMessage(`Successfully unwrapped ${tokenAmount.toString()} WETH to ETH`); - balance = balance.minus(value); } if (!this.props.isOutdatedWrappedEther) { - this.props.dispatcher.replaceTokenBalanceByAddress(token.address, balance); + this.props.refetchEthTokenStateAsync(); } this.props.onConversionSuccessful(); } catch (err) { diff --git a/packages/website/ts/components/eth_wrappers.tsx b/packages/website/ts/components/eth_wrappers.tsx index d074ec787..460a6cae3 100644 --- a/packages/website/ts/components/eth_wrappers.tsx +++ b/packages/website/ts/components/eth_wrappers.tsx @@ -41,12 +41,14 @@ interface EthWrappersProps { blockchain: Blockchain; dispatcher: Dispatcher; tokenByAddress: TokenByAddress; - tokenStateByAddress: TokenStateByAddress; userAddress: string; userEtherBalance: BigNumber; + lastForceTokenStateRefetch: number; } interface EthWrappersState { + ethTokenState: TokenState; + isWethStateLoaded: boolean; outdatedWETHAddressToIsStateLoaded: OutdatedWETHAddressToIsStateLoaded; outdatedWETHStateByAddress: OutdatedWETHStateByAddress; } @@ -67,18 +69,31 @@ export class EthWrappers extends React.Component {this._renderTokenLink(tokenLabel, etherscanUrl)} - {wethBalance.toFixed(PRECISION)} WETH + + {this.state.isWethStateLoaded ? ( + `${wethBalance.toFixed(PRECISION)} WETH` + ) : ( + + )} + - {this._renderOutdatedWeths(etherToken, etherTokenState)} + {this._renderOutdatedWeths(etherToken, this.state.ethTokenState)} @@ -269,6 +299,10 @@ export class EthWrappers extends React.Component token.symbol === 'WETH'); + const [wethBalance, wethAllowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync( + this.props.userAddress, + wethToken.address, + ); + const outdatedWETHAddresses = this._getOutdatedWETHAddresses(); const outdatedWETHAddressToIsStateLoaded: OutdatedWETHAddressToIsStateLoaded = {}; const outdatedWETHStateByAddress: OutdatedWETHStateByAddress = {}; @@ -356,6 +397,11 @@ export class EthWrappers extends React.Component { symbol: takerToken.symbol, }; const fillToken = this.props.tokenByAddress[takerToken.address]; - const fillTokenState = this.props.tokenStateByAddress[takerToken.address]; const makerTokenAddress = this.state.parsedOrder.maker.token.address; const makerToken = this.props.tokenByAddress[makerTokenAddress]; const makerAssetToken = { @@ -249,14 +248,17 @@ export class FillOrder extends React.Component { {!isUserMaker && (
{ signedOrder, this.props.orderFillAmount, ); - // After fill completes, let's update the token balances - const makerToken = this.props.tokenByAddress[parsedOrder.maker.token.address]; - const takerToken = this.props.tokenByAddress[parsedOrder.taker.token.address]; - const tokens = [makerToken, takerToken]; - await this.props.blockchain.updateTokenBalancesAndAllowancesAsync(tokens); + // After fill completes, let's force fetch the token balances + this.props.dispatcher.forceTokenStateRefetch(); this.setState({ isFilling: false, didFillOrderSucceed: true, diff --git a/packages/website/ts/components/generate_order/asset_picker.tsx b/packages/website/ts/components/generate_order/asset_picker.tsx index df7d87cfd..8d4eaab40 100644 --- a/packages/website/ts/components/generate_order/asset_picker.tsx +++ b/packages/website/ts/components/generate_order/asset_picker.tsx @@ -223,10 +223,7 @@ export class AssetPicker extends React.Component void; + onNewTokenSubmitted: (token: Token) => void; } interface NewTokenFormState { @@ -110,13 +110,9 @@ export class NewTokenForm extends React.Component Promise; } interface AllowanceToggleState { @@ -45,7 +47,7 @@ export class AllowanceToggle extends React.Component
@@ -73,6 +75,7 @@ export class AllowanceToggle extends React.Component InputErrMsg; onVisitBalancesPageClick?: () => void; shouldHideVisitBalancesLink?: boolean; + isDisabled?: boolean; } interface BalanceBoundedInputState { @@ -29,6 +30,7 @@ export class BalanceBoundedInput extends React.Component = { shouldShowIncompleteErrs: false, shouldHideVisitBalancesLink: false, + isDisabled: false, }; constructor(props: BalanceBoundedInputProps) { super(props); @@ -88,6 +90,7 @@ export class BalanceBoundedInput extends React.Componentamount} onChange={this._onValueChange.bind(this)} underlineStyle={{ width: 'calc(100% + 50px)' }} + disabled={this.props.isDisabled} /> ); } diff --git a/packages/website/ts/components/inputs/token_amount_input.tsx b/packages/website/ts/components/inputs/token_amount_input.tsx index 63966d759..f41d42d02 100644 --- a/packages/website/ts/components/inputs/token_amount_input.tsx +++ b/packages/website/ts/components/inputs/token_amount_input.tsx @@ -3,13 +3,16 @@ import { BigNumber } from '@0xproject/utils'; import * as _ from 'lodash'; import * as React from 'react'; import { Link } from 'react-router-dom'; +import { Blockchain } from 'ts/blockchain'; import { BalanceBoundedInput } from 'ts/components/inputs/balance_bounded_input'; import { InputErrMsg, Token, TokenState, ValidatedBigNumberCallback, WebsitePaths } from 'ts/types'; import { colors } from 'ts/utils/colors'; interface TokenAmountInputProps { + userAddress: string; + networkId: number; + blockchain: Blockchain; token: Token; - tokenState: TokenState; label?: string; amount?: BigNumber; shouldShowIncompleteErrs: boolean; @@ -17,11 +20,39 @@ interface TokenAmountInputProps { shouldCheckAllowance: boolean; onChange: ValidatedBigNumberCallback; onVisitBalancesPageClick?: () => void; + lastForceTokenStateRefetch: number; } -interface TokenAmountInputState {} +interface TokenAmountInputState { + balance: BigNumber; + allowance: BigNumber; + isBalanceAndAllowanceLoaded: boolean; +} export class TokenAmountInput extends React.Component { + constructor(props: TokenAmountInputProps) { + super(props); + const defaultAmount = new BigNumber(0); + this.state = { + balance: defaultAmount, + allowance: defaultAmount, + isBalanceAndAllowanceLoaded: false, + }; + } + public componentWillMount() { + // tslint:disable-next-line:no-floating-promises + this._fetchBalanceAndAllowanceAsync(this.props.token.address); + } + public componentWillReceiveProps(nextProps: TokenAmountInputProps) { + if ( + nextProps.userAddress !== this.props.userAddress || + nextProps.networkId !== this.props.networkId || + nextProps.token.address !== this.props.token.address || + nextProps.lastForceTokenStateRefetch !== this.props.lastForceTokenStateRefetch + ) { + this._fetchBalanceAndAllowanceAsync(nextProps.token.address); + } + } public render() { const amount = this.props.amount ? ZeroEx.toUnitAmount(this.props.amount, this.props.token.decimals) @@ -32,12 +63,13 @@ export class TokenAmountInput extends React.Component
{this.props.token.symbol}
@@ -51,7 +83,7 @@ export class TokenAmountInput extends React.Component Insufficient allowance.{' '} @@ -67,4 +99,18 @@ export class TokenAmountInput extends React.Component { if (nextProps.userAddress !== this.state.prevUserAddress) { // tslint:disable-next-line:no-floating-promises this._blockchain.userAddressUpdatedFireAndForgetAsync(nextProps.userAddress); - if (!_.isEmpty(nextProps.userAddress) && nextProps.blockchainIsLoaded) { - const tokens = _.values(nextProps.tokenByAddress); - const trackedTokens = _.filter(tokens, t => t.isTracked); - // tslint:disable-next-line:no-floating-promises - this._updateBalanceAndAllowanceWithLoadingScreenAsync(trackedTokens); - } this.setState({ prevUserAddress: nextProps.userAddress, }); @@ -280,9 +274,9 @@ export class Portal extends React.Component { blockchain={this._blockchain} dispatcher={this.props.dispatcher} tokenByAddress={this.props.tokenByAddress} - tokenStateByAddress={this.props.tokenStateByAddress} userAddress={this.props.userAddress} userEtherBalance={this.props.userEtherBalance} + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} /> ); } @@ -296,6 +290,8 @@ export class Portal extends React.Component { ); } private _renderTokenBalances() { + const allTokens = _.values(this.props.tokenByAddress); + const trackedTokens = _.filter(allTokens, t => t.isTracked); return ( { dispatcher={this.props.dispatcher} screenWidth={this.props.screenWidth} tokenByAddress={this.props.tokenByAddress} - tokenStateByAddress={this.props.tokenStateByAddress} + trackedTokens={trackedTokens} userAddress={this.props.userAddress} userEtherBalance={this.props.userEtherBalance} networkId={this.props.networkId} + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} /> ); } @@ -325,8 +322,8 @@ export class Portal extends React.Component { networkId={this.props.networkId} userAddress={this.props.userAddress} tokenByAddress={this.props.tokenByAddress} - tokenStateByAddress={this.props.tokenStateByAddress} dispatcher={this.props.dispatcher} + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} /> ); } @@ -382,9 +379,4 @@ export class Portal extends React.Component { const newScreenWidth = utils.getScreenWidth(); this.props.dispatcher.updateScreenWidth(newScreenWidth); } - private async _updateBalanceAndAllowanceWithLoadingScreenAsync(tokens: Token[]) { - this.props.dispatcher.updateBlockchainIsLoaded(false); - await this._blockchain.updateTokenBalancesAndAllowancesAsync(tokens); - this.props.dispatcher.updateBlockchainIsLoaded(true); - } } diff --git a/packages/website/ts/components/send_button.tsx b/packages/website/ts/components/send_button.tsx index 7c0c7c083..f13e8ecce 100644 --- a/packages/website/ts/components/send_button.tsx +++ b/packages/website/ts/components/send_button.tsx @@ -10,11 +10,15 @@ import { errorReporter } from 'ts/utils/error_reporter'; import { utils } from 'ts/utils/utils'; interface SendButtonProps { + userAddress: string; + networkId: number; token: Token; tokenState: TokenState; dispatcher: Dispatcher; blockchain: Blockchain; onError: () => void; + lastForceTokenStateRefetch: number; + refetchTokenStateAsync: (tokenAddress: string) => Promise; } interface SendButtonState { @@ -42,11 +46,14 @@ export class SendButton extends React.Component
); @@ -63,11 +70,9 @@ export class SendButton extends React.Component { public constructor(props: TokenBalancesProps) { super(props); + const initialTrackedTokenStateByAddress = this._getInitialTrackedTokenStateByAddress(props.trackedTokens); this.state = { errorType: undefined, isBalanceSpinnerVisible: false, @@ -91,8 +100,14 @@ export class TokenBalances extends React.Component t.symbol === ZRX_TOKEN_SYMBOL); - const nextZrxTokenBalance = nextProps.tokenStateByAddress[nextZrxToken.address].balance; - if (!_.isUndefined(this.state.currentZrxBalance) && !nextZrxTokenBalance.eq(this.state.currentZrxBalance)) { - if (this.state.isZRXSpinnerVisible) { - const receivedAmount = nextZrxTokenBalance.minus(this.state.currentZrxBalance); - const receiveAmountInUnits = ZeroEx.toUnitAmount(receivedAmount, constants.DECIMAL_PLACES_ZRX); - this.props.dispatcher.showFlashMessage(`Received ${receiveAmountInUnits.toString(10)} Kovan ZRX`); - } - this.setState({ - isZRXSpinnerVisible: false, - currentZrxBalance: undefined, - }); + + 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); + this._fetchBalancesAndAllowancesAsync(newTokenAddresses); } } public componentDidMount() { @@ -303,8 +321,7 @@ export class TokenBalances extends React.Component t.isTracked); + 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) @@ -317,7 +334,7 @@ export class TokenBalances extends React.Component - {this._renderAmount(tokenState.balance, token.decimals)} {token.symbol} - {this.state.isZRXSpinnerVisible && - token.symbol === ZRX_TOKEN_SYMBOL && ( - - - - )} + {tokenState.isLoaded ? ( + + {this._renderAmount(tokenState.balance, token.decimals)} {token.symbol} + {this.state.isZRXSpinnerVisible && + token.symbol === ZRX_TOKEN_SYMBOL && ( + + + + )} + + ) : ( + + )} @@ -383,11 +408,15 @@ export class TokenBalances extends React.Component )} @@ -414,7 +443,6 @@ export class TokenBalances extends React.Component { try { await this.props.blockchain.mintTestTokensAsync(token); + await this._refetchTokenStateAsync(token.address); const amount = ZeroEx.toUnitAmount(constants.MINT_AMOUNT, token.decimals); this.props.dispatcher.showFlashMessage(`Successfully minted ${amount.toString(10)} ${token.symbol}`); return true; @@ -569,15 +598,11 @@ export class TokenBalances extends React.Component t.symbol === ZRX_TOKEN_SYMBOL); - const zrxTokenState = this.props.tokenStateByAddress[zrxToken.address]; this.setState({ isZRXSpinnerVisible: true, - currentZrxBalance: zrxTokenState.balance, }); // tslint:disable-next-line:no-floating-promises - this.props.blockchain.pollTokenBalanceAsync(zrxToken); + this._startPollingZrxBalanceAsync(); } return true; } @@ -603,4 +628,63 @@ export class TokenBalances extends React.Component t.symbol === ZRX_TOKEN_SYMBOL); + + // tslint:disable-next-line:no-floating-promises + const balance = await this.props.blockchain.pollTokenBalanceAsync(zrxToken); + const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress; + trackedTokenStateByAddress[zrxToken.address] = { + ...trackedTokenStateByAddress[zrxToken.address], + balance, + }; + this.setState({ + isZRXSpinnerVisible: false, + }); + } + private async _fetchBalancesAndAllowancesAsync(tokenAddresses: string[]) { + const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress; + for (const tokenAddress of tokenAddresses) { + const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync( + this.props.userAddress, + tokenAddress, + ); + trackedTokenStateByAddress[tokenAddress] = { + balance, + allowance, + isLoaded: true, + }; + } + this.setState({ + trackedTokenStateByAddress, + }); + } + 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 _refetchTokenStateAsync(tokenAddress: string) { + const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync( + this.props.userAddress, + tokenAddress, + ); + this.setState({ + trackedTokenStateByAddress: { + ...this.state.trackedTokenStateByAddress, + [tokenAddress]: { + balance, + allowance, + isLoaded: true, + }, + }, + }); + } } // tslint:disable:max-file-line-count diff --git a/packages/website/ts/components/top_bar/provider_picker.tsx b/packages/website/ts/components/top_bar/provider_picker.tsx index ca98d8d05..418f8696b 100644 --- a/packages/website/ts/components/top_bar/provider_picker.tsx +++ b/packages/website/ts/components/top_bar/provider_picker.tsx @@ -81,7 +81,7 @@ export class ProviderPicker extends React.Component ({ @@ -45,8 +45,8 @@ const mapStateToProps = (state: State, ownProps: GenerateOrderFormProps): Connec networkId: state.networkId, sideToAssetToken: state.sideToAssetToken, tokenByAddress: state.tokenByAddress, - tokenStateByAddress: state.tokenStateByAddress, userAddress: state.userAddress, + lastForceTokenStateRefetch: state.lastForceTokenStateRefetch, }); export const GenerateOrderForm: React.ComponentClass = connect(mapStateToProps)( diff --git a/packages/website/ts/containers/portal.tsx b/packages/website/ts/containers/portal.tsx index 2c515aac4..9afe7fca3 100644 --- a/packages/website/ts/containers/portal.tsx +++ b/packages/website/ts/containers/portal.tsx @@ -28,7 +28,7 @@ interface ConnectedState { orderFillAmount: BigNumber; providerType: ProviderType; tokenByAddress: TokenByAddress; - tokenStateByAddress: TokenStateByAddress; + lastForceTokenStateRefetch: number; userEtherBalance: BigNumber; screenWidth: ScreenWidths; shouldBlockchainErrDialogBeOpen: boolean; @@ -77,7 +77,7 @@ const mapStateToProps = (state: State, ownProps: PortalComponentAllProps): Conne screenWidth: state.screenWidth, shouldBlockchainErrDialogBeOpen: state.shouldBlockchainErrDialogBeOpen, tokenByAddress: state.tokenByAddress, - tokenStateByAddress: state.tokenStateByAddress, + lastForceTokenStateRefetch: state.lastForceTokenStateRefetch, userAddress: state.userAddress, userEtherBalance: state.userEtherBalance, userSuppliedOrderCache: state.userSuppliedOrderCache, diff --git a/packages/website/ts/redux/dispatcher.ts b/packages/website/ts/redux/dispatcher.ts index 42989e5e1..dea0a8bfe 100644 --- a/packages/website/ts/redux/dispatcher.ts +++ b/packages/website/ts/redux/dispatcher.ts @@ -131,43 +131,9 @@ export class Dispatcher { type: ActionTypes.UpdateTokenByAddress, }); } - public updateTokenStateByAddress(tokenStateByAddress: TokenStateByAddress) { + public forceTokenStateRefetch() { this._dispatch({ - data: tokenStateByAddress, - type: ActionTypes.UpdateTokenStateByAddress, - }); - } - public removeFromTokenStateByAddress(tokenAddress: string) { - this._dispatch({ - data: tokenAddress, - type: ActionTypes.RemoveFromTokenStateByAddress, - }); - } - public replaceTokenAllowanceByAddress(address: string, allowance: BigNumber) { - this._dispatch({ - data: { - address, - allowance, - }, - type: ActionTypes.ReplaceTokenAllowanceByAddress, - }); - } - public replaceTokenBalanceByAddress(address: string, balance: BigNumber) { - this._dispatch({ - data: { - address, - balance, - }, - type: ActionTypes.ReplaceTokenBalanceByAddress, - }); - } - public updateTokenBalanceByAddress(address: string, balanceDelta: BigNumber) { - this._dispatch({ - data: { - address, - balanceDelta, - }, - type: ActionTypes.UpdateTokenBalanceByAddress, + type: ActionTypes.ForceTokenStateRefetch, }); } public updateSignatureData(signatureData: SignatureData) { diff --git a/packages/website/ts/redux/reducer.ts b/packages/website/ts/redux/reducer.ts index 06ac8b670..cee475fa9 100644 --- a/packages/website/ts/redux/reducer.ts +++ b/packages/website/ts/redux/reducer.ts @@ -1,6 +1,7 @@ import { ZeroEx } from '0x.js'; import { BigNumber } from '@0xproject/utils'; import * as _ from 'lodash'; +import * as moment from 'moment'; import { Action, ActionTypes, @@ -37,7 +38,7 @@ export interface State { shouldBlockchainErrDialogBeOpen: boolean; sideToAssetToken: SideToAssetToken; tokenByAddress: TokenByAddress; - tokenStateByAddress: TokenStateByAddress; + lastForceTokenStateRefetch: number; userAddress: string; userEtherBalance: BigNumber; // Note: cache of supplied orderJSON in fill order step. Do not use for anything else. @@ -76,7 +77,7 @@ const INITIAL_STATE: State = { [Side.Receive]: {}, }, tokenByAddress: {}, - tokenStateByAddress: {}, + lastForceTokenStateRefetch: moment().unix(), userAddress: '', userEtherBalance: new BigNumber(0), userSuppliedOrderCache: undefined, @@ -180,74 +181,11 @@ export function reducer(state: State = INITIAL_STATE, action: Action) { }; } - case ActionTypes.UpdateTokenStateByAddress: { - const tokenStateByAddress = state.tokenStateByAddress; - const updatedTokenStateByAddress = action.data; - _.each(updatedTokenStateByAddress, (tokenState: TokenState, address: string) => { - const updatedTokenState = { - ...tokenStateByAddress[address], - ...tokenState, - }; - tokenStateByAddress[address] = updatedTokenState; - }); - return { - ...state, - tokenStateByAddress, - }; - } - - case ActionTypes.RemoveFromTokenStateByAddress: { - const tokenStateByAddress = state.tokenStateByAddress; - const tokenAddress = action.data; - delete tokenStateByAddress[tokenAddress]; - return { - ...state, - tokenStateByAddress, - }; - } - - case ActionTypes.ReplaceTokenAllowanceByAddress: { - const tokenStateByAddress = state.tokenStateByAddress; - const allowance = action.data.allowance; - const tokenAddress = action.data.address; - tokenStateByAddress[tokenAddress] = { - ...tokenStateByAddress[tokenAddress], - allowance, - }; + case ActionTypes.ForceTokenStateRefetch: return { ...state, - tokenStateByAddress, + lastForceTokenStateRefetch: moment().unix(), }; - } - - case ActionTypes.ReplaceTokenBalanceByAddress: { - const tokenStateByAddress = state.tokenStateByAddress; - const balance = action.data.balance; - const tokenAddress = action.data.address; - tokenStateByAddress[tokenAddress] = { - ...tokenStateByAddress[tokenAddress], - balance, - }; - return { - ...state, - tokenStateByAddress, - }; - } - - case ActionTypes.UpdateTokenBalanceByAddress: { - const tokenStateByAddress = state.tokenStateByAddress; - const balanceDelta = action.data.balanceDelta; - const tokenAddress = action.data.address; - const currBalance = tokenStateByAddress[tokenAddress].balance; - tokenStateByAddress[tokenAddress] = { - ...tokenStateByAddress[tokenAddress], - balance: currBalance.plus(balanceDelta), - }; - return { - ...state, - tokenStateByAddress, - }; - } case ActionTypes.UpdateOrderSignatureData: { return { diff --git a/packages/website/ts/types.ts b/packages/website/ts/types.ts index aec8f7e1e..134d4d7bf 100644 --- a/packages/website/ts/types.ts +++ b/packages/website/ts/types.ts @@ -125,11 +125,7 @@ export enum ActionTypes { UpdateOrderSignatureData = 'UPDATE_ORDER_SIGNATURE_DATA', UpdateTokenByAddress = 'UPDATE_TOKEN_BY_ADDRESS', RemoveTokenFromTokenByAddress = 'REMOVE_TOKEN_FROM_TOKEN_BY_ADDRESS', - UpdateTokenStateByAddress = 'UPDATE_TOKEN_STATE_BY_ADDRESS', - RemoveFromTokenStateByAddress = 'REMOVE_FROM_TOKEN_STATE_BY_ADDRESS', - ReplaceTokenAllowanceByAddress = 'REPLACE_TOKEN_ALLOWANCE_BY_ADDRESS', - ReplaceTokenBalanceByAddress = 'REPLACE_TOKEN_BALANCE_BY_ADDRESS', - UpdateTokenBalanceByAddress = 'UPDATE_TOKEN_BALANCE_BY_ADDRESS', + ForceTokenStateRefetch = 'FORCE_TOKEN_STATE_REFETCH', UpdateOrderExpiry = 'UPDATE_ORDER_EXPIRY', SwapAssetTokens = 'SWAP_ASSET_TOKENS', UpdateUserAddress = 'UPDATE_USER_ADDRESS', -- cgit v1.2.3