aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorFabio Berger <me@fabioberger.com>2018-01-28 23:19:55 +0800
committerFabio Berger <me@fabioberger.com>2018-01-28 23:19:55 +0800
commit6206ebc994a2cf76b90ac426218d6ed18b74a072 (patch)
treef8246d6ef94126af9f5c8fc3bdc7b52712397c9f /packages
parentdd9f5adc2e771f3602461ae708d44536f146b902 (diff)
downloaddexon-0x-contracts-6206ebc994a2cf76b90ac426218d6ed18b74a072.tar
dexon-0x-contracts-6206ebc994a2cf76b90ac426218d6ed18b74a072.tar.gz
dexon-0x-contracts-6206ebc994a2cf76b90ac426218d6ed18b74a072.tar.bz2
dexon-0x-contracts-6206ebc994a2cf76b90ac426218d6ed18b74a072.tar.lz
dexon-0x-contracts-6206ebc994a2cf76b90ac426218d6ed18b74a072.tar.xz
dexon-0x-contracts-6206ebc994a2cf76b90ac426218d6ed18b74a072.tar.zst
dexon-0x-contracts-6206ebc994a2cf76b90ac426218d6ed18b74a072.zip
Implement just-in-time loading of token balances & allowances
Diffstat (limited to 'packages')
-rw-r--r--packages/website/ts/blockchain.ts59
-rw-r--r--packages/website/ts/components/dialogs/eth_weth_conversion_dialog.tsx60
-rw-r--r--packages/website/ts/components/dialogs/ledger_config_dialog.tsx3
-rw-r--r--packages/website/ts/components/dialogs/send_dialog.tsx13
-rw-r--r--packages/website/ts/components/dialogs/track_token_confirmation_dialog.tsx10
-rw-r--r--packages/website/ts/components/eth_weth_conversion_button.tsx14
-rw-r--r--packages/website/ts/components/eth_wrappers.tsx86
-rw-r--r--packages/website/ts/components/fill_order.tsx17
-rw-r--r--packages/website/ts/components/generate_order/asset_picker.tsx14
-rw-r--r--packages/website/ts/components/generate_order/generate_order_form.tsx20
-rw-r--r--packages/website/ts/components/generate_order/new_token_form.tsx14
-rw-r--r--packages/website/ts/components/inputs/allowance_toggle.tsx5
-rw-r--r--packages/website/ts/components/inputs/balance_bounded_input.tsx3
-rw-r--r--packages/website/ts/components/inputs/token_amount_input.tsx54
-rw-r--r--packages/website/ts/components/portal.tsx22
-rw-r--r--packages/website/ts/components/send_button.tsx13
-rw-r--r--packages/website/ts/components/token_balances.tsx146
-rw-r--r--packages/website/ts/components/top_bar/provider_picker.tsx2
-rw-r--r--packages/website/ts/containers/generate_order_form.tsx4
-rw-r--r--packages/website/ts/containers/portal.tsx4
-rw-r--r--packages/website/ts/redux/dispatcher.ts38
-rw-r--r--packages/website/ts/redux/reducer.ts72
-rw-r--r--packages/website/ts/types.ts6
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',