diff options
author | Fabio Berger <me@fabioberger.com> | 2018-01-28 23:19:55 +0800 |
---|---|---|
committer | Fabio Berger <me@fabioberger.com> | 2018-01-28 23:19:55 +0800 |
commit | 6206ebc994a2cf76b90ac426218d6ed18b74a072 (patch) | |
tree | f8246d6ef94126af9f5c8fc3bdc7b52712397c9f | |
parent | dd9f5adc2e771f3602461ae708d44536f146b902 (diff) | |
download | dexon-sol-tools-6206ebc994a2cf76b90ac426218d6ed18b74a072.tar.gz dexon-sol-tools-6206ebc994a2cf76b90ac426218d6ed18b74a072.tar.zst dexon-sol-tools-6206ebc994a2cf76b90ac426218d6ed18b74a072.zip |
Implement just-in-time loading of token balances & allowances
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<void> { 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<void> { 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<SignatureData> { 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<BigNumber> { 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 = [ <FlatButton key="cancel" label="Cancel" onTouchTap={this._onCancel.bind(this)} />, @@ -72,8 +84,11 @@ export class EthWethConversionDialog extends React.Component< <div className="pt2 mx-auto" style={{ width: 245 }}> {this.props.direction === Side.Receive ? ( <TokenAmountInput + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} + blockchain={this.props.blockchain} + userAddress={this.props.userAddress} + networkId={this.props.networkId} token={this.props.token} - tokenState={this.props.tokenState} shouldShowIncompleteErrs={this.state.shouldShowIncompleteErrs} shouldCheckBalance={true} shouldCheckAllowance={false} @@ -93,19 +108,20 @@ export class EthWethConversionDialog extends React.Component< )} <div className="pt1" style={{ fontSize: 12 }}> <div className="left">1 ETH = 1 WETH</div> - {this.props.direction === Side.Receive && ( - <div - className="right" - onClick={this._onMaxClick.bind(this)} - style={{ - color: colors.darkBlue, - textDecoration: 'underline', - cursor: 'pointer', - }} - > - Max - </div> - )} + {this.props.direction === Side.Receive && + this.state.isEthTokenBalanceLoaded && ( + <div + className="right" + onClick={this._onMaxClick.bind(this)} + style={{ + color: colors.darkBlue, + textDecoration: 'underline', + cursor: 'pointer', + }} + > + Max + </div> + )} </div> </div> </div> @@ -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<LedgerConfigDialogProps, const selectAddressBalance = this.state.addressBalances[selectedRowIndex]; this.props.dispatcher.updateUserAddress(selectedAddress); this.props.blockchain.updateWeb3WrapperPrevUserAddress(selectedAddress); - this.props.blockchain.fetchTokenInformationAsync(); // fire and forget + // tslint:disable-next-line:no-floating-promises + this.props.blockchain.fetchTokenInformationAsync(); this.props.dispatcher.updateUserEtherBalance(selectAddressBalance); this.setState({ stepIndex: LedgerSteps.CONNECT, diff --git a/packages/website/ts/components/dialogs/send_dialog.tsx b/packages/website/ts/components/dialogs/send_dialog.tsx index b3dbce598..d44dd9aab 100644 --- a/packages/website/ts/components/dialogs/send_dialog.tsx +++ b/packages/website/ts/components/dialogs/send_dialog.tsx @@ -3,16 +3,20 @@ import * as _ from 'lodash'; import Dialog from 'material-ui/Dialog'; import FlatButton from 'material-ui/FlatButton'; import * as React from 'react'; +import { Blockchain } from 'ts/blockchain'; import { AddressInput } from 'ts/components/inputs/address_input'; import { TokenAmountInput } from 'ts/components/inputs/token_amount_input'; -import { Token, TokenState } from 'ts/types'; +import { Token } from 'ts/types'; interface SendDialogProps { + blockchain: Blockchain; + userAddress: string; + networkId: number; onComplete: (recipient: string, value: BigNumber) => void; onCancelled: () => void; isOpen: boolean; token: Token; - tokenState: TokenState; + lastForceTokenStateRefetch: number; } interface SendDialogState { @@ -66,15 +70,18 @@ export class SendDialog extends React.Component<SendDialogProps, SendDialogState /> </div> <TokenAmountInput + blockchain={this.props.blockchain} + userAddress={this.props.userAddress} + networkId={this.props.networkId} label="Amount to send" token={this.props.token} - tokenState={this.props.tokenState} shouldShowIncompleteErrs={this.state.shouldShowIncompleteErrs} shouldCheckBalance={true} shouldCheckAllowance={false} onChange={this._onValueChange.bind(this)} amount={this.state.value} onVisitBalancesPageClick={this.props.onCancelled} + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} /> </div> ); 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<void>; } interface EthWethConversionButtonState { @@ -64,13 +68,16 @@ export class EthWethConversionButton extends React.Component< onClick={this._toggleConversionDialog.bind(this)} /> <EthWethConversionDialog + blockchain={this.props.blockchain} + userAddress={this.props.userAddress} + networkId={this.props.networkId} direction={this.props.direction} isOpen={this.state.isEthConversionDialogVisible} onComplete={this._onConversionAmountSelectedAsync.bind(this)} onCancelled={this._toggleConversionDialog.bind(this)} etherBalance={this.props.userEtherBalance} token={this.props.ethToken} - tokenState={this.props.ethTokenState} + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} /> </div> ); @@ -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<EthWrappersProps, EthWrappersSt this.state = { outdatedWETHAddressToIsStateLoaded, outdatedWETHStateByAddress, + isWethStateLoaded: false, + ethTokenState: { + balance: new BigNumber(0), + allowance: new BigNumber(0), + }, }; } + public componentWillReceiveProps(nextProps: EthWrappersProps) { + if ( + nextProps.userAddress !== this.props.userAddress || + nextProps.networkId !== this.props.networkId || + nextProps.lastForceTokenStateRefetch !== this.props.lastForceTokenStateRefetch + ) { + // tslint:disable-next-line:no-floating-promises + this._fetchWETHStateAsync(); + } + } public componentDidMount() { window.scrollTo(0, 0); // tslint:disable-next-line:no-floating-promises - this._fetchOutdatedWETHStateAsync(); + this._fetchWETHStateAsync(); } public render() { - const tokens = _.values(this.props.tokenByAddress); - const etherToken = _.find(tokens, { symbol: 'WETH' }); - const etherTokenState = this.props.tokenStateByAddress[etherToken.address]; - const wethBalance = ZeroEx.toUnitAmount(etherTokenState.balance, constants.DECIMAL_PLACES_ETH); + const etherToken = this._getEthToken(); + const wethBalance = ZeroEx.toUnitAmount(this.state.ethTokenState.balance, constants.DECIMAL_PLACES_ETH); const isBidirectional = true; const etherscanUrl = utils.getEtherScanLinkIfExists( etherToken.address, @@ -136,10 +151,14 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt </TableRowColumn> <TableRowColumn> <EthWethConversionButton + refetchEthTokenStateAsync={this._refetchEthTokenStateAsync.bind(this)} + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} + userAddress={this.props.userAddress} + networkId={this.props.networkId} isOutdatedWrappedEther={false} direction={Side.Deposit} ethToken={etherToken} - ethTokenState={etherTokenState} + ethTokenState={this.state.ethTokenState} dispatcher={this.props.dispatcher} blockchain={this.props.blockchain} userEtherBalance={this.props.userEtherBalance} @@ -150,13 +169,24 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt <TableRowColumn className="py1"> {this._renderTokenLink(tokenLabel, etherscanUrl)} </TableRowColumn> - <TableRowColumn>{wethBalance.toFixed(PRECISION)} WETH</TableRowColumn> + <TableRowColumn> + {this.state.isWethStateLoaded ? ( + `${wethBalance.toFixed(PRECISION)} WETH` + ) : ( + <i className="zmdi zmdi-spinner zmdi-hc-spin" /> + )} + </TableRowColumn> <TableRowColumn> <EthWethConversionButton + refetchEthTokenStateAsync={this._refetchEthTokenStateAsync.bind(this)} + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} + userAddress={this.props.userAddress} + networkId={this.props.networkId} isOutdatedWrappedEther={false} direction={Side.Receive} + isDisabled={!this.state.isWethStateLoaded} ethToken={etherToken} - ethTokenState={etherTokenState} + ethTokenState={this.state.ethTokenState} dispatcher={this.props.dispatcher} blockchain={this.props.blockchain} userEtherBalance={this.props.userEtherBalance} @@ -190,7 +220,7 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt </TableRow> </TableHeader> <TableBody displayRowCheckbox={false}> - {this._renderOutdatedWeths(etherToken, etherTokenState)} + {this._renderOutdatedWeths(etherToken, this.state.ethTokenState)} </TableBody> </Table> </div> @@ -269,6 +299,10 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt </TableRowColumn> <TableRowColumn> <EthWethConversionButton + refetchEthTokenStateAsync={this._refetchEthTokenStateAsync.bind(this)} + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} + userAddress={this.props.userAddress} + networkId={this.props.networkId} isDisabled={!isStateLoaded} isOutdatedWrappedEther={true} direction={Side.Receive} @@ -338,7 +372,14 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt }, }); } - private async _fetchOutdatedWETHStateAsync() { + private async _fetchWETHStateAsync() { + const tokens = _.values(this.props.tokenByAddress); + const wethToken = _.find(tokens, token => 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<EthWrappersProps, EthWrappersSt this.setState({ outdatedWETHStateByAddress, outdatedWETHAddressToIsStateLoaded, + ethTokenState: { + balance: wethBalance, + allowance: wethAllowance, + }, + isWethStateLoaded: true, }); } private _getOutdatedWETHAddresses(): string[] { @@ -371,4 +417,22 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt ); return outdatedWETHAddresses; } + private _getEthToken() { + const tokens = _.values(this.props.tokenByAddress); + const etherToken = _.find(tokens, { symbol: 'WETH' }); + return etherToken; + } + private async _refetchEthTokenStateAsync() { + const etherToken = this._getEthToken(); + const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync( + this.props.userAddress, + etherToken.address, + ); + this.setState({ + ethTokenState: { + balance, + allowance, + }, + }); + } } // tslint:disable:max-file-line-count diff --git a/packages/website/ts/components/fill_order.tsx b/packages/website/ts/components/fill_order.tsx index 28488f399..1e9927c1a 100644 --- a/packages/website/ts/components/fill_order.tsx +++ b/packages/website/ts/components/fill_order.tsx @@ -19,7 +19,7 @@ import { VisualOrder } from 'ts/components/visual_order'; import { Dispatcher } from 'ts/redux/dispatcher'; import { orderSchema } from 'ts/schemas/order_schema'; import { SchemaValidator } from 'ts/schemas/validator'; -import { AlertTypes, BlockchainErrs, Order, Token, TokenByAddress, TokenStateByAddress, WebsitePaths } from 'ts/types'; +import { AlertTypes, BlockchainErrs, Order, Token, TokenByAddress, WebsitePaths } from 'ts/types'; import { colors } from 'ts/utils/colors'; import { constants } from 'ts/utils/constants'; import { errorReporter } from 'ts/utils/error_reporter'; @@ -33,9 +33,9 @@ interface FillOrderProps { networkId: number; userAddress: string; tokenByAddress: TokenByAddress; - tokenStateByAddress: TokenStateByAddress; initialOrder: Order; dispatcher: Dispatcher; + lastForceTokenStateRefetch: number; } interface FillOrderState { @@ -185,7 +185,6 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> { 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<FillOrderProps, FillOrderState> { {!isUserMaker && ( <div className="clearfix mx-auto relative" style={{ width: 235, height: 108 }}> <TokenAmountInput + blockchain={this.props.blockchain} + userAddress={this.props.userAddress} + networkId={this.props.networkId} label="Fill amount" onChange={this._onFillAmountChange.bind(this)} shouldShowIncompleteErrs={false} token={fillToken} - tokenState={fillTokenState} amount={fillAssetToken.amount} shouldCheckBalance={true} shouldCheckAllowance={true} + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} /> <div className="absolute sm-hide xs-hide" @@ -556,11 +558,8 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> { 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<AssetPickerProps, AssetPickerSt assetView: AssetViews.NEW_TOKEN_FORM, }); } - private _onNewTokenSubmitted(newToken: Token, newTokenState: TokenState) { - this.props.dispatcher.updateTokenStateByAddress({ - [newToken.address]: newTokenState, - }); + private _onNewTokenSubmitted(newToken: Token) { trackedTokenStorage.addTrackedTokenToUser(this.props.userAddress, this.props.networkId, newToken); this.props.dispatcher.addTokenToTokenByAddress(newToken); this.setState({ @@ -256,15 +253,6 @@ export class AssetPicker extends React.Component<AssetPickerProps, AssetPickerSt newTokenEntry.isTracked = true; trackedTokenStorage.addTrackedTokenToUser(this.props.userAddress, this.props.networkId, newTokenEntry); - const [balance, allowance] = await this.props.blockchain.getCurrentUserTokenBalanceAndAllowanceAsync( - token.address, - ); - this.props.dispatcher.updateTokenStateByAddress({ - [token.address]: { - balance, - allowance, - }, - }); this.props.dispatcher.updateTokenByAddress([newTokenEntry]); this.setState({ isAddingTokenToTracked: false, diff --git a/packages/website/ts/components/generate_order/generate_order_form.tsx b/packages/website/ts/components/generate_order/generate_order_form.tsx index 3ae0d48a7..1f6b4c6c0 100644 --- a/packages/website/ts/components/generate_order/generate_order_form.tsx +++ b/packages/website/ts/components/generate_order/generate_order_form.tsx @@ -53,7 +53,7 @@ interface GenerateOrderFormProps { orderSalt: BigNumber; sideToAssetToken: SideToAssetToken; tokenByAddress: TokenByAddress; - tokenStateByAddress: TokenStateByAddress; + lastForceTokenStateRefetch: number; } interface GenerateOrderFormState { @@ -80,10 +80,8 @@ export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, G const dispatcher = this.props.dispatcher; const depositTokenAddress = this.props.sideToAssetToken[Side.Deposit].address; const depositToken = this.props.tokenByAddress[depositTokenAddress]; - const depositTokenState = this.props.tokenStateByAddress[depositTokenAddress]; const receiveTokenAddress = this.props.sideToAssetToken[Side.Receive].address; const receiveToken = this.props.tokenByAddress[receiveTokenAddress]; - const receiveTokenState = this.props.tokenStateByAddress[receiveTokenAddress]; const takerExplanation = 'If a taker is specified, only they are<br> \ allowed to fill this order. If no taker is<br> \ @@ -110,9 +108,12 @@ export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, G tokenByAddress={this.props.tokenByAddress} /> <TokenAmountInput + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} + blockchain={this.props.blockchain} + userAddress={this.props.userAddress} + networkId={this.props.networkId} label="Sell amount" token={depositToken} - tokenState={depositTokenState} amount={this.props.sideToAssetToken[Side.Deposit].amount} onChange={this._onTokenAmountChange.bind(this, depositToken, Side.Deposit)} shouldShowIncompleteErrs={this.state.shouldShowIncompleteErrs} @@ -139,9 +140,12 @@ export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, G tokenByAddress={this.props.tokenByAddress} /> <TokenAmountInput + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} + blockchain={this.props.blockchain} + userAddress={this.props.userAddress} + networkId={this.props.networkId} label="Receive amount" token={receiveToken} - tokenState={receiveTokenState} amount={this.props.sideToAssetToken[Side.Receive].amount} onChange={this._onTokenAmountChange.bind(this, receiveToken, Side.Receive)} shouldShowIncompleteErrs={this.state.shouldShowIncompleteErrs} @@ -242,8 +246,10 @@ export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, G // Check if all required inputs were supplied const debitToken = this.props.sideToAssetToken[Side.Deposit]; - const debitBalance = this.props.tokenStateByAddress[debitToken.address].balance; - const debitAllowance = this.props.tokenStateByAddress[debitToken.address].allowance; + const [debitBalance, debitAllowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync( + this.props.userAddress, + debitToken.address, + ); const receiveAmount = this.props.sideToAssetToken[Side.Receive].amount; if ( !_.isUndefined(debitToken.amount) && diff --git a/packages/website/ts/components/generate_order/new_token_form.tsx b/packages/website/ts/components/generate_order/new_token_form.tsx index 63645be9a..44712b27b 100644 --- a/packages/website/ts/components/generate_order/new_token_form.tsx +++ b/packages/website/ts/components/generate_order/new_token_form.tsx @@ -13,7 +13,7 @@ import { colors } from 'ts/utils/colors'; interface NewTokenFormProps { blockchain: Blockchain; tokenByAddress: TokenByAddress; - onNewTokenSubmitted: (token: Token, tokenState: TokenState) => void; + onNewTokenSubmitted: (token: Token) => void; } interface NewTokenFormState { @@ -110,13 +110,9 @@ export class NewTokenForm extends React.Component<NewTokenFormProps, NewTokenFor } let hasBalanceAllowanceErr = false; - let balance = new BigNumber(0); - let allowance = new BigNumber(0); if (doesContractExist) { try { - [balance, allowance] = await this.props.blockchain.getCurrentUserTokenBalanceAndAllowanceAsync( - this.state.address, - ); + await this.props.blockchain.getCurrentUserTokenBalanceAndAllowanceAsync(this.state.address); } catch (err) { hasBalanceAllowanceErr = true; } @@ -155,11 +151,7 @@ export class NewTokenForm extends React.Component<NewTokenFormProps, NewTokenFor isTracked: true, isRegistered: false, }; - const newTokenState: TokenState = { - balance, - allowance, - }; - this.props.onNewTokenSubmitted(newToken, newTokenState); + this.props.onNewTokenSubmitted(newToken); } private _onTokenNameChanged(e: any, name: string) { let nameErrText = ''; diff --git a/packages/website/ts/components/inputs/allowance_toggle.tsx b/packages/website/ts/components/inputs/allowance_toggle.tsx index d8852c11f..45531e74b 100644 --- a/packages/website/ts/components/inputs/allowance_toggle.tsx +++ b/packages/website/ts/components/inputs/allowance_toggle.tsx @@ -17,6 +17,8 @@ interface AllowanceToggleProps { token: Token; tokenState: TokenState; userAddress: string; + isDisabled: boolean; + refetchTokenStateAsync: () => Promise<void>; } interface AllowanceToggleState { @@ -45,7 +47,7 @@ export class AllowanceToggle extends React.Component<AllowanceToggleProps, Allow <div className="flex"> <div> <Toggle - disabled={this.state.isSpinnerVisible} + disabled={this.state.isSpinnerVisible || this.props.isDisabled} toggled={this._isAllowanceSet()} onToggle={this._onToggleAllowanceAsync.bind(this)} /> @@ -73,6 +75,7 @@ export class AllowanceToggle extends React.Component<AllowanceToggleProps, Allow } try { await this.props.blockchain.setProxyAllowanceAsync(this.props.token, newAllowanceAmountInBaseUnits); + await this.props.refetchTokenStateAsync(); } catch (err) { this.setState({ isSpinnerVisible: false, diff --git a/packages/website/ts/components/inputs/balance_bounded_input.tsx b/packages/website/ts/components/inputs/balance_bounded_input.tsx index ddc434b51..67d5b781e 100644 --- a/packages/website/ts/components/inputs/balance_bounded_input.tsx +++ b/packages/website/ts/components/inputs/balance_bounded_input.tsx @@ -18,6 +18,7 @@ interface BalanceBoundedInputProps { validate?: (amount: BigNumber) => InputErrMsg; onVisitBalancesPageClick?: () => void; shouldHideVisitBalancesLink?: boolean; + isDisabled?: boolean; } interface BalanceBoundedInputState { @@ -29,6 +30,7 @@ export class BalanceBoundedInput extends React.Component<BalanceBoundedInputProp public static defaultProps: Partial<BalanceBoundedInputProps> = { shouldShowIncompleteErrs: false, shouldHideVisitBalancesLink: false, + isDisabled: false, }; constructor(props: BalanceBoundedInputProps) { super(props); @@ -88,6 +90,7 @@ export class BalanceBoundedInput extends React.Component<BalanceBoundedInputProp hintText={<span style={{ textTransform: 'capitalize' }}>amount</span>} 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<TokenAmountInputProps, TokenAmountInputState> { + 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<TokenAmountInputProps, Tok <BalanceBoundedInput label={this.props.label} amount={amount} - balance={ZeroEx.toUnitAmount(this.props.tokenState.balance, this.props.token.decimals)} + balance={ZeroEx.toUnitAmount(this.state.balance, this.props.token.decimals)} onChange={this._onChange.bind(this)} validate={this._validate.bind(this)} shouldCheckBalance={this.props.shouldCheckBalance} shouldShowIncompleteErrs={this.props.shouldShowIncompleteErrs} onVisitBalancesPageClick={this.props.onVisitBalancesPageClick} + isDisabled={!this.state.isBalanceAndAllowanceLoaded} /> <div style={{ paddingTop: hasLabel ? 39 : 14 }}>{this.props.token.symbol}</div> </div> @@ -51,7 +83,7 @@ export class TokenAmountInput extends React.Component<TokenAmountInputProps, Tok this.props.onChange(isValid, baseUnitAmount); } private _validate(amount: BigNumber): InputErrMsg { - if (this.props.shouldCheckAllowance && amount.gt(this.props.tokenState.allowance)) { + if (this.props.shouldCheckAllowance && amount.gt(this.state.allowance)) { return ( <span> Insufficient allowance.{' '} @@ -67,4 +99,18 @@ export class TokenAmountInput extends React.Component<TokenAmountInputProps, Tok return undefined; } } + private async _fetchBalanceAndAllowanceAsync(tokenAddress: string) { + this.setState({ + isBalanceAndAllowanceLoaded: false, + }); + const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync( + this.props.userAddress, + tokenAddress, + ); + this.setState({ + balance, + allowance, + isBalanceAndAllowanceLoaded: true, + }); + } } diff --git a/packages/website/ts/components/portal.tsx b/packages/website/ts/components/portal.tsx index 5975569c4..e8190e2f5 100644 --- a/packages/website/ts/components/portal.tsx +++ b/packages/website/ts/components/portal.tsx @@ -56,13 +56,13 @@ export interface PortalAllProps { providerType: ProviderType; screenWidth: ScreenWidths; tokenByAddress: TokenByAddress; - tokenStateByAddress: TokenStateByAddress; userEtherBalance: BigNumber; userAddress: string; shouldBlockchainErrDialogBeOpen: boolean; userSuppliedOrderCache: Order; location: Location; flashMessage?: string | React.ReactNode; + lastForceTokenStateRefetch: number; } interface PortalAllState { @@ -132,12 +132,6 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> { 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<PortalAllProps, PortalAllState> { 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<PortalAllProps, PortalAllState> { ); } private _renderTokenBalances() { + const allTokens = _.values(this.props.tokenByAddress); + const trackedTokens = _.filter(allTokens, t => t.isTracked); return ( <TokenBalances blockchain={this._blockchain} @@ -304,10 +300,11 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> { 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<PortalAllProps, PortalAllState> { 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<PortalAllProps, PortalAllState> { 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<void>; } interface SendButtonState { @@ -42,11 +46,14 @@ export class SendButton extends React.Component<SendButtonProps, SendButtonState onClick={this._toggleSendDialog.bind(this)} /> <SendDialog + blockchain={this.props.blockchain} + userAddress={this.props.userAddress} + networkId={this.props.networkId} isOpen={this.state.isSendDialogVisible} onComplete={this._onSendAmountSelectedAsync.bind(this)} onCancelled={this._toggleSendDialog.bind(this)} token={this.props.token} - tokenState={this.props.tokenState} + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} /> </div> ); @@ -63,11 +70,9 @@ export class SendButton extends React.Component<SendButtonProps, SendButtonState this._toggleSendDialog(); const token = this.props.token; const tokenState = this.props.tokenState; - let balance = tokenState.balance; try { await this.props.blockchain.transferAsync(token, recipient, value); - balance = balance.minus(value); - this.props.dispatcher.replaceTokenBalanceByAddress(token.address, balance); + this.props.refetchTokenStateAsync(token.address); } catch (err) { const errMsg = `${err}`; if (_.includes(errMsg, BlockchainCallErrs.UserHasNoAssociatedAddresses)) { diff --git a/packages/website/ts/components/token_balances.tsx b/packages/website/ts/components/token_balances.tsx index 88f86fbf5..5b450c772 100644 --- a/packages/website/ts/components/token_balances.tsx +++ b/packages/website/ts/components/token_balances.tsx @@ -31,7 +31,6 @@ import { Styles, Token, TokenByAddress, - TokenStateByAddress, TokenVisibility, } from 'ts/types'; import { colors } from 'ts/utils/colors'; @@ -58,6 +57,14 @@ const styles: Styles = { }, }; +interface TokenStateByAddress { + [address: string]: { + balance: BigNumber; + allowance: BigNumber; + isLoaded: boolean; + }; +} + interface TokenBalancesProps { blockchain: Blockchain; blockchainErr: BlockchainErrs; @@ -65,10 +72,11 @@ interface TokenBalancesProps { dispatcher: Dispatcher; screenWidth: ScreenWidths; tokenByAddress: TokenByAddress; - tokenStateByAddress: TokenStateByAddress; + trackedTokens: Token[]; userAddress: string; userEtherBalance: BigNumber; networkId: number; + lastForceTokenStateRefetch: number; } interface TokenBalancesState { @@ -76,14 +84,15 @@ interface TokenBalancesState { isBalanceSpinnerVisible: boolean; isDharmaDialogVisible: boolean; isZRXSpinnerVisible: boolean; - currentZrxBalance?: BigNumber; isTokenPickerOpen: boolean; isAddingToken: boolean; + trackedTokenStateByAddress: TokenStateByAddress; } export class TokenBalances extends React.Component<TokenBalancesProps, TokenBalancesState> { 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<TokenBalancesProps, TokenBala isDharmaDialogVisible: DharmaLoanFrame.isAuthTokenPresent(), isTokenPickerOpen: false, isAddingToken: false, + trackedTokenStateByAddress: initialTrackedTokenStateByAddress, }; } + public componentWillMount() { + // tslint:disable-next-line:no-floating-promises + const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress); + this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses); + } public componentWillReceiveProps(nextProps: TokenBalancesProps) { if (nextProps.userEtherBalance !== this.props.userEtherBalance) { if (this.state.isBalanceSpinnerVisible) { @@ -103,18 +118,21 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala isBalanceSpinnerVisible: false, }); } - const nextZrxToken = _.find(_.values(nextProps.tokenByAddress), t => 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<TokenBalancesProps, TokenBala const isSmallScreen = this.props.screenWidth === ScreenWidths.Sm; const tokenColSpan = isSmallScreen ? TOKEN_COL_SPAN_SM : TOKEN_COL_SPAN_LG; const actionPaddingX = isSmallScreen ? 2 : 24; - const allTokens = _.values(this.props.tokenByAddress); - const trackedTokens = _.filter(allTokens, t => 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<TokenBalancesProps, TokenBala return tableRows; } private _renderTokenRow(tokenColSpan: number, actionPaddingX: number, token: Token) { - const tokenState = this.props.tokenStateByAddress[token.address]; + const tokenState = this.state.trackedTokenStateByAddress[token.address]; const tokenLink = utils.getEtherScanLinkIfExists( token.address, this.props.networkId, @@ -338,13 +355,19 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala )} </TableRowColumn> <TableRowColumn style={{ paddingRight: 3, paddingLeft: 3 }}> - {this._renderAmount(tokenState.balance, token.decimals)} {token.symbol} - {this.state.isZRXSpinnerVisible && - token.symbol === ZRX_TOKEN_SYMBOL && ( - <span className="pl1"> - <i className="zmdi zmdi-spinner zmdi-hc-spin" /> - </span> - )} + {tokenState.isLoaded ? ( + <span> + {this._renderAmount(tokenState.balance, token.decimals)} {token.symbol} + {this.state.isZRXSpinnerVisible && + token.symbol === ZRX_TOKEN_SYMBOL && ( + <span className="pl1"> + <i className="zmdi zmdi-spinner zmdi-hc-spin" /> + </span> + )} + </span> + ) : ( + <i className="zmdi zmdi-spinner zmdi-hc-spin" /> + )} </TableRowColumn> <TableRowColumn> <AllowanceToggle @@ -354,6 +377,8 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala tokenState={tokenState} onErrorOccurred={this._onErrorOccurred.bind(this)} userAddress={this.props.userAddress} + isDisabled={!tokenState.isLoaded} + refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this, token.address)} /> </TableRowColumn> <TableRowColumn style={{ paddingLeft: actionPaddingX, paddingRight: actionPaddingX }}> @@ -383,11 +408,15 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala }} > <SendButton + userAddress={this.props.userAddress} + networkId={this.props.networkId} blockchain={this.props.blockchain} dispatcher={this.props.dispatcher} token={token} tokenState={tokenState} onError={this._onSendFailed.bind(this)} + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} + refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this, token.address)} /> </TableRowColumn> )} @@ -414,7 +443,6 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala } else { this.props.dispatcher.removeTokenToTokenByAddress(token); } - this.props.dispatcher.removeFromTokenStateByAddress(tokenAddress); 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`); @@ -510,6 +538,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala private async _onMintTestTokensAsync(token: Token): Promise<boolean> { 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<TokenBalancesProps, TokenBala isBalanceSpinnerVisible: true, }); } else { - const tokens = _.values(this.props.tokenByAddress); - const zrxToken = _.find(tokens, t => 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<TokenBalancesProps, TokenBala isAddingToken: false, }); } + private async _startPollingZrxBalanceAsync() { + const tokens = _.values(this.props.tokenByAddress); + const zrxToken = _.find(tokens, t => 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<ProviderPickerProps, Provide if (value === ProviderType.Ledger) { this.props.onToggleLedgerDialog(); } else { - // Fire and forget + // tslint:disable-next-line:no-floating-promises this.props.blockchain.updateProviderToInjectedAsync(); } } diff --git a/packages/website/ts/containers/generate_order_form.tsx b/packages/website/ts/containers/generate_order_form.tsx index 3fd31087f..af9f0da01 100644 --- a/packages/website/ts/containers/generate_order_form.tsx +++ b/packages/website/ts/containers/generate_order_form.tsx @@ -32,7 +32,7 @@ interface ConnectedState { networkId: number; sideToAssetToken: SideToAssetToken; tokenByAddress: TokenByAddress; - tokenStateByAddress: TokenStateByAddress; + lastForceTokenStateRefetch: number; } const mapStateToProps = (state: State, ownProps: GenerateOrderFormProps): ConnectedState => ({ @@ -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<GenerateOrderFormProps> = 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', |