Merge pull request #644 from 0xProject/feature/website/refactor-token-state-fetching

Move TokenState fetching logic up into Portal.tsx so it can be used by Wallet and PortalOnboardingFlow
This commit is contained in:
Francesco Agosti
2018-06-04 10:46:08 -07:00
committed by GitHub
5 changed files with 127 additions and 97 deletions

View File

@@ -506,7 +506,7 @@ export class Blockchain {
public async getTokenBalanceAndAllowanceAsync(
ownerAddressIfExists: string,
tokenAddress: string,
): Promise<BigNumber[]> {
): Promise<[BigNumber, BigNumber]> {
utils.assert(!_.isUndefined(this._contractWrappers), 'ContractWrappers must be instantiated.');
if (_.isUndefined(ownerAddressIfExists)) {
@@ -516,8 +516,10 @@ export class Blockchain {
let balance = new BigNumber(0);
let allowance = new BigNumber(0);
if (this._doesUserAddressExist()) {
balance = await this._contractWrappers.token.getBalanceAsync(tokenAddress, ownerAddressIfExists);
allowance = await this._contractWrappers.token.getProxyAllowanceAsync(tokenAddress, ownerAddressIfExists);
[balance, allowance] = await Promise.all([
this._contractWrappers.token.getBalanceAsync(tokenAddress, ownerAddressIfExists),
this._contractWrappers.token.getProxyAllowanceAsync(tokenAddress, ownerAddressIfExists),
]);
}
return [balance, allowance];
}

View File

@@ -3,7 +3,7 @@ import * as React from 'react';
import { BigNumber } from '@0xproject/utils';
import { OnboardingFlow, Step } from 'ts/components/onboarding/onboarding_flow';
import { ProviderType, TokenByAddress } from 'ts/types';
import { ProviderType, TokenByAddress, TokenStateByAddress } from 'ts/types';
import { utils } from 'ts/utils/utils';
export interface PortalOnboardingFlowProps {
@@ -16,6 +16,7 @@ export interface PortalOnboardingFlowProps {
blockchainIsLoaded: boolean;
userEtherBalanceInWei?: BigNumber;
tokenByAddress: TokenByAddress;
trackedTokenStateByAddress: TokenStateByAddress;
updateIsRunning: (isRunning: boolean) => void;
updateOnboardingStep: (stepIndex: number) => void;
}
@@ -68,13 +69,13 @@ export class PortalOnboardingFlow extends React.Component<PortalOnboardingFlowPr
target: '.eth-row',
content: 'Before you begin you will need to send some ETH to your metamask wallet',
placement: 'right',
continueButtonDisplay: this._userHasEth() ? 'enabled' : 'disabled',
continueButtonDisplay: this._userHasVisibleEth() ? 'enabled' : 'disabled',
},
{
target: '.weth-row',
content: 'You need to convert some of your ETH into tradeable Wrapped ETH (WETH)',
placement: 'right',
continueButtonDisplay: this._userHasWeth() ? 'enabled' : 'disabled',
continueButtonDisplay: this._userHasVisibleWeth() ? 'enabled' : 'disabled',
},
];
return steps;
@@ -84,13 +85,17 @@ export class PortalOnboardingFlow extends React.Component<PortalOnboardingFlowPr
return !_.isEmpty(this.props.userAddress);
}
private _userHasEth(): boolean {
private _userHasVisibleEth(): boolean {
return this.props.userEtherBalanceInWei > new BigNumber(0);
}
private _userHasWeth(): boolean {
// TODO: https://app.asana.com/0/681385331277907/690722374136933
return false;
private _userHasVisibleWeth(): boolean {
const ethToken = utils.getEthToken(this.props.tokenByAddress);
if (!ethToken) {
return false;
}
const wethTokenState = this.props.trackedTokenStateByAddress[ethToken.address];
return wethTokenState.balance > new BigNumber(0);
}
private _overrideOnboardingStateIfShould(): void {

View File

@@ -37,7 +37,9 @@ import {
Order,
ProviderType,
ScreenWidths,
Token,
TokenByAddress,
TokenStateByAddress,
TokenVisibility,
WebsitePaths,
} from 'ts/types';
@@ -77,6 +79,7 @@ interface PortalState {
isDisclaimerDialogOpen: boolean;
isLedgerDialogOpen: boolean;
tokenManagementState: TokenManagementState;
trackedTokenStateByAddress: TokenStateByAddress;
}
interface AccountManagementItem {
@@ -127,6 +130,9 @@ export class Portal extends React.Component<PortalProps, PortalState> {
const didAcceptPortalDisclaimer = localStorage.getItemIfExists(constants.LOCAL_STORAGE_KEY_ACCEPT_DISCLAIMER);
const hasAcceptedDisclaimer =
!_.isUndefined(didAcceptPortalDisclaimer) && !_.isEmpty(didAcceptPortalDisclaimer);
const initialTrackedTokenStateByAddress = this._getInitialTrackedTokenStateByAddress(
this._getCurrentTrackedTokens(),
);
this.state = {
prevNetworkId: this.props.networkId,
prevNodeVersion: this.props.nodeVersion,
@@ -135,6 +141,7 @@ export class Portal extends React.Component<PortalProps, PortalState> {
isDisclaimerDialogOpen: !hasAcceptedDisclaimer,
tokenManagementState: TokenManagementState.None,
isLedgerDialogOpen: false,
trackedTokenStateByAddress: initialTrackedTokenStateByAddress,
};
}
public componentDidMount(): void {
@@ -143,6 +150,9 @@ export class Portal extends React.Component<PortalProps, PortalState> {
}
public componentWillMount(): void {
this._blockchain = new Blockchain(this.props.dispatcher);
const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress);
// tslint:disable-next-line:no-floating-promises
this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses);
}
public componentWillUnmount(): void {
this._blockchain.destroy();
@@ -178,6 +188,39 @@ export class Portal extends React.Component<PortalProps, PortalState> {
prevPathname: nextProps.location.pathname,
});
}
if (
nextProps.userAddress !== this.props.userAddress ||
nextProps.networkId !== this.props.networkId ||
nextProps.lastForceTokenStateRefetch !== this.props.lastForceTokenStateRefetch
) {
const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress);
// tslint:disable-next-line:no-floating-promises
this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses);
}
const nextTrackedTokens = this._getTrackedTokens(nextProps.tokenByAddress);
const trackedTokens = this._getCurrentTrackedTokens();
if (!_.isEqual(nextTrackedTokens, trackedTokens)) {
const newTokens = _.difference(nextTrackedTokens, trackedTokens);
const newTokenAddresses = _.map(newTokens, token => token.address);
// Add placeholder entry for this token to the state, since fetching the
// balance/allowance is asynchronous
const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress;
for (const tokenAddress of newTokenAddresses) {
trackedTokenStateByAddress[tokenAddress] = {
balance: new BigNumber(0),
allowance: new BigNumber(0),
isLoaded: false,
};
}
this.setState({
trackedTokenStateByAddress,
});
// Fetch the actual balance/allowance.
// tslint:disable-next-line:no-floating-promises
this._fetchBalancesAndAllowancesAsync(newTokenAddresses);
}
}
public render(): React.ReactNode {
const updateShouldBlockchainErrDialogBeOpen = this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen.bind(
@@ -190,7 +233,7 @@ export class Portal extends React.Component<PortalProps, PortalState> {
: TokenVisibility.TRACKED;
return (
<div style={styles.root}>
<PortalOnboardingFlow />
<PortalOnboardingFlow trackedTokenStateByAddress={this.state.trackedTokenStateByAddress} />
<DocumentTitle title="0x Portal DApp" />
<TopBar
userAddress={this.props.userAddress}
@@ -283,8 +326,6 @@ export class Portal extends React.Component<PortalProps, PortalState> {
);
}
private _renderWallet(): React.ReactNode {
const allTokens = _.values(this.props.tokenByAddress);
const trackedTokens = _.filter(allTokens, t => t.isTracked);
return (
<div>
<Wallet
@@ -295,16 +336,18 @@ export class Portal extends React.Component<PortalProps, PortalState> {
blockchainErr={this.props.blockchainErr}
dispatcher={this.props.dispatcher}
tokenByAddress={this.props.tokenByAddress}
trackedTokens={trackedTokens}
trackedTokens={this._getCurrentTrackedTokens()}
userEtherBalanceInWei={this.props.userEtherBalanceInWei}
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
injectedProviderName={this.props.injectedProviderName}
providerType={this.props.providerType}
screenWidth={this.props.screenWidth}
location={this.props.location}
trackedTokenStateByAddress={this.state.trackedTokenStateByAddress}
onToggleLedgerDialog={this._onToggleLedgerDialog.bind(this)}
onAddToken={this._onAddToken.bind(this)}
onRemoveToken={this._onRemoveToken.bind(this)}
refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this)}
/>
<Container marginTop="15px">
<Island>
@@ -424,8 +467,6 @@ export class Portal extends React.Component<PortalProps, PortalState> {
);
}
private _renderTokenBalances(): React.ReactNode {
const allTokens = _.values(this.props.tokenByAddress);
const trackedTokens = _.filter(allTokens, t => t.isTracked);
return (
<TokenBalances
blockchain={this._blockchain}
@@ -434,7 +475,7 @@ export class Portal extends React.Component<PortalProps, PortalState> {
dispatcher={this.props.dispatcher}
screenWidth={this.props.screenWidth}
tokenByAddress={this.props.tokenByAddress}
trackedTokens={trackedTokens}
trackedTokens={this._getCurrentTrackedTokens()}
userAddress={this.props.userAddress}
userEtherBalanceInWei={this.props.userEtherBalanceInWei}
networkId={this.props.networkId}
@@ -515,6 +556,55 @@ export class Portal extends React.Component<PortalProps, PortalState> {
const isSmallScreen = this.props.screenWidth === ScreenWidths.Sm;
return isSmallScreen;
}
private _getCurrentTrackedTokens(): Token[] {
return this._getTrackedTokens(this.props.tokenByAddress);
}
private _getTrackedTokens(tokenByAddress: TokenByAddress): Token[] {
const allTokens = _.values(tokenByAddress);
const trackedTokens = _.filter(allTokens, t => t.isTracked);
return trackedTokens;
}
private _getInitialTrackedTokenStateByAddress(trackedTokens: Token[]): TokenStateByAddress {
const trackedTokenStateByAddress: TokenStateByAddress = {};
_.each(trackedTokens, token => {
trackedTokenStateByAddress[token.address] = {
balance: new BigNumber(0),
allowance: new BigNumber(0),
isLoaded: false,
};
});
return trackedTokenStateByAddress;
}
private async _fetchBalancesAndAllowancesAsync(tokenAddresses: string[]): Promise<void> {
const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress;
const userAddressIfExists = _.isEmpty(this.props.userAddress) ? undefined : this.props.userAddress;
const balancesAndAllowances = await Promise.all(
tokenAddresses.map(async tokenAddress => {
return this._blockchain.getTokenBalanceAndAllowanceAsync(userAddressIfExists, tokenAddress);
}),
);
for (let i = 0; i < tokenAddresses.length; i++) {
// Order is preserved in Promise.all
const [balance, allowance] = balancesAndAllowances[i];
const tokenAddress = tokenAddresses[i];
trackedTokenStateByAddress[tokenAddress] = {
balance,
allowance,
isLoaded: true,
};
}
this.setState({
trackedTokenStateByAddress,
});
}
private async _refetchTokenStateAsync(tokenAddress: string): Promise<void> {
await this._fetchBalancesAndAllowancesAsync([tokenAddress]);
}
}
interface LargeLayoutProps {

View File

@@ -67,13 +67,14 @@ export interface WalletProps {
providerType: ProviderType;
screenWidth: ScreenWidths;
location: Location;
trackedTokenStateByAddress: TokenStateByAddress;
onToggleLedgerDialog: () => void;
onAddToken: () => void;
onRemoveToken: () => void;
refetchTokenStateAsync: (tokenAddress: string) => Promise<void>;
}
interface WalletState {
trackedTokenStateByAddress: TokenStateByAddress;
wrappedEtherDirection?: Side;
isHoveringSidebar: boolean;
}
@@ -151,48 +152,12 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
constructor(props: WalletProps) {
super(props);
this._isUnmounted = false;
const trackedTokenAddresses = _.map(props.trackedTokens, token => token.address);
const initialTrackedTokenStateByAddress = this._getInitialTrackedTokenStateByAddress(trackedTokenAddresses);
this.state = {
trackedTokenStateByAddress: initialTrackedTokenStateByAddress,
wrappedEtherDirection: undefined,
isHoveringSidebar: false,
};
}
public componentWillMount(): void {
const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress);
// tslint:disable-next-line:no-floating-promises
this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses);
}
public componentWillUnmount(): void {
this._isUnmounted = true;
}
public componentWillReceiveProps(nextProps: WalletProps): void {
if (
nextProps.userAddress !== this.props.userAddress ||
nextProps.networkId !== this.props.networkId ||
nextProps.lastForceTokenStateRefetch !== this.props.lastForceTokenStateRefetch
) {
const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress);
// tslint:disable-next-line:no-floating-promises
this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses);
}
if (!_.isEqual(nextProps.trackedTokens, this.props.trackedTokens)) {
const newTokens = _.difference(nextProps.trackedTokens, this.props.trackedTokens);
const newTokenAddresses = _.map(newTokens, token => token.address);
// Add placeholder entry for this token to the state, since fetching the
// balance/allowance is asynchronous
const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress;
const initialTrackedTokenStateByAddress = this._getInitialTrackedTokenStateByAddress(newTokenAddresses);
_.assign(trackedTokenStateByAddress, initialTrackedTokenStateByAddress);
this.setState({
trackedTokenStateByAddress,
});
// Fetch the actual balance/allowance.
// tslint:disable-next-line:no-floating-promises
this._fetchBalancesAndAllowancesAsync(newTokenAddresses);
}
}
public render(): React.ReactNode {
const isBlockchainLoaded = this.props.blockchainIsLoaded && this.props.blockchainErr === BlockchainErrs.NoError;
return (
@@ -342,7 +307,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
_.isUndefined(this.props.userEtherBalanceInWei),
);
const etherToken = this._getEthToken();
const etherTokenState = this.state.trackedTokenStateByAddress[etherToken.address];
const etherTokenState = this.props.trackedTokenStateByAddress[etherToken.address];
const etherPrice = etherTokenState.price;
const secondaryText = this._renderValue(
this.props.userEtherBalanceInWei || new BigNumber(0),
@@ -366,7 +331,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
return _.map(trackedTokensStartingWithEtherToken, this._renderTokenRow.bind(this));
}
private _renderTokenRow(token: Token, index: number): React.ReactNode {
const tokenState = this.state.trackedTokenStateByAddress[token.address];
const tokenState = this.props.trackedTokenStateByAddress[token.address];
const tokenLink = sharedUtils.getEtherScanLinkIfExists(
token.address,
this.props.networkId,
@@ -438,7 +403,8 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
etherToken={etherToken}
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
onConversionSuccessful={this._closeWrappedEtherActionRow.bind(this)}
refetchEthTokenStateAsync={this._refetchTokenStateAsync.bind(this, etherToken.address)}
// tslint:disable:jsx-no-lambda
refetchEthTokenStateAsync={async () => this.props.refetchTokenStateAsync(etherToken.address)}
/>
)}
</div>
@@ -474,7 +440,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
onErrorOccurred={_.noop} // TODO: Error handling
userAddress={this.props.userAddress}
isDisabled={!config.tokenState.isLoaded}
refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this, config.token.address)}
refetchTokenStateAsync={async () => this.props.refetchTokenStateAsync(config.token.address)}
/>
);
}
@@ -557,42 +523,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
});
return trackedTokenStateByAddress;
}
private async _fetchBalancesAndAllowancesAsync(tokenAddresses: string[]): Promise<void> {
const balanceAndAllowanceTupleByAddress: ItemByAddress<BigNumber[]> = {};
const userAddressIfExists = _.isEmpty(this.props.userAddress) ? undefined : this.props.userAddress;
for (const tokenAddress of tokenAddresses) {
const balanceAndAllowanceTuple = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
userAddressIfExists,
tokenAddress,
);
balanceAndAllowanceTupleByAddress[tokenAddress] = balanceAndAllowanceTuple;
}
const priceByAddress = await this._getPriceByAddressAsync(tokenAddresses);
const trackedTokenStateByAddress = _.reduce(
tokenAddresses,
(acc, address) => {
const [balance, allowance] = balanceAndAllowanceTupleByAddress[address];
const priceIfExists = _.get(priceByAddress, address);
acc[address] = {
balance,
allowance,
price: priceIfExists,
isLoaded: true,
};
return acc;
},
this.state.trackedTokenStateByAddress,
);
if (!this._isUnmounted) {
this.setState({
trackedTokenStateByAddress,
});
}
}
private async _refetchTokenStateAsync(tokenAddress: string): Promise<void> {
await this._fetchBalancesAndAllowancesAsync([tokenAddress]);
}
private async _getPriceByAddressAsync(tokenAddresses: string[]): Promise<ItemByAddress<BigNumber>> {
if (_.isEmpty(tokenAddresses)) {
return {};

View File

@@ -2,12 +2,14 @@ import { BigNumber } from '@0xproject/utils';
import * as React from 'react';
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import { ActionTypes, ProviderType, TokenByAddress } from 'ts/types';
import { ActionTypes, ProviderType, TokenByAddress, TokenStateByAddress } from 'ts/types';
import { PortalOnboardingFlow as PortalOnboardingFlowComponent } from 'ts/components/onboarding/portal_onboarding_flow';
import { State } from 'ts/redux/reducer';
interface PortalOnboardingFlowProps {}
interface PortalOnboardingFlowProps {
trackedTokenStateByAddress: TokenStateByAddress;
}
interface ConnectedState {
stepIndex: number;
@@ -26,7 +28,7 @@ interface ConnectedDispatch {
updateOnboardingStep: (stepIndex: number) => void;
}
const mapStateToProps = (state: State): ConnectedState => ({
const mapStateToProps = (state: State, ownProps: PortalOnboardingFlowProps): ConnectedState => ({
stepIndex: state.portalOnboardingStep,
isRunning: state.isPortalOnboardingShowing,
userAddress: state.userAddress,