Skip to content
This repository has been archived by the owner on Jul 9, 2021. It is now read-only.

[instant] Request account address and balance at mount #1232

Merged
merged 5 commits into from
Nov 9, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 12 additions & 16 deletions packages/instant/src/components/buy_button.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AssetBuyer, AssetBuyerError, BuyQuote } from '@0x/asset-buyer';
import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import * as _ from 'lodash';
import * as React from 'react';
Expand All @@ -7,16 +8,17 @@ import { oc } from 'ts-optchain';
import { WEB_3_WRAPPER_TRANSACTION_FAILED_ERROR_MSG_PREFIX } from '../constants';
import { ColorOption } from '../style/theme';
import { AffiliateInfo, ZeroExInstantError } from '../types';
import { getBestAddress } from '../util/address';
import { balanceUtil } from '../util/balance';
import { gasPriceEstimator } from '../util/gas_price_estimator';
import { util } from '../util/util';

import { Button } from './ui/button';

export interface BuyButtonProps {
accountAddress?: string;
accountEthBalanceInWei?: BigNumber;
buyQuote?: BuyQuote;
assetBuyer: AssetBuyer;
web3Wrapper: Web3Wrapper;
affiliateInfo?: AffiliateInfo;
onValidationPending: (buyQuote: BuyQuote) => void;
onValidationFail: (buyQuote: BuyQuote, errorMessage: AssetBuyerError | ZeroExInstantError) => void;
Expand All @@ -33,7 +35,8 @@ export class BuyButton extends React.Component<BuyButtonProps> {
onBuyFailure: util.boundNoop,
};
public render(): React.ReactNode {
const shouldDisableButton = _.isUndefined(this.props.buyQuote);
const { buyQuote, accountAddress } = this.props;
const shouldDisableButton = _.isUndefined(buyQuote) || _.isUndefined(accountAddress);
return (
<Button
width="100%"
Expand All @@ -48,30 +51,25 @@ export class BuyButton extends React.Component<BuyButtonProps> {
}
private readonly _handleClick = async () => {
// The button is disabled when there is no buy quote anyway.
const { buyQuote, assetBuyer, affiliateInfo } = this.props;
if (_.isUndefined(buyQuote)) {
const { buyQuote, assetBuyer, affiliateInfo, accountAddress, accountEthBalanceInWei, web3Wrapper } = this.props;
if (_.isUndefined(buyQuote) || _.isUndefined(accountAddress)) {
return;
}

this.props.onValidationPending(buyQuote);

// TODO(bmillman): move address and balance fetching to the async state
const web3Wrapper = new Web3Wrapper(assetBuyer.provider);
const takerAddress = await getBestAddress(web3Wrapper);

const hasSufficientEth = await balanceUtil.hasSufficientEth(takerAddress, buyQuote, web3Wrapper);
const ethNeededForBuy = buyQuote.worstCaseQuoteInfo.totalEthAmount;
// if we don't have a balance for the user, let the transaction through, it will be handled by the wallet
const hasSufficientEth = _.isUndefined(accountEthBalanceInWei) || accountEthBalanceInWei.gte(ethNeededForBuy);
if (!hasSufficientEth) {
this.props.onValidationFail(buyQuote, ZeroExInstantError.InsufficientETH);
return;
}

let txHash: string | undefined;
const gasInfo = await gasPriceEstimator.getGasInfoAsync();
const feeRecipient = oc(affiliateInfo).feeRecipient();
try {
txHash = await assetBuyer.executeBuyQuoteAsync(buyQuote, {
feeRecipient,
takerAddress,
takerAddress: accountAddress,
gasPrice: gasInfo.gasPriceInWei,
});
} catch (e) {
Expand All @@ -86,7 +84,6 @@ export class BuyButton extends React.Component<BuyButtonProps> {
}
throw e;
}

const startTimeUnix = new Date().getTime();
const expectedEndTimeUnix = startTimeUnix + gasInfo.estimatedTimeMs;
this.props.onBuyProcessing(buyQuote, txHash, startTimeUnix, expectedEndTimeUnix);
Expand All @@ -99,7 +96,6 @@ export class BuyButton extends React.Component<BuyButtonProps> {
}
throw e;
}

this.props.onBuySuccess(buyQuote, txHash);
};
}
8 changes: 8 additions & 0 deletions packages/instant/src/components/buy_order_state_buttons.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { AssetBuyer, AssetBuyerError, BuyQuote } from '@0x/asset-buyer';
import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import * as React from 'react';

import { ColorOption } from '../style/theme';
Expand All @@ -12,9 +14,12 @@ import { Button } from './ui/button';
import { Flex } from './ui/flex';

export interface BuyOrderStateButtonProps {
accountAddress?: string;
accountEthBalanceInWei?: BigNumber;
buyQuote?: BuyQuote;
buyOrderProcessingState: OrderProcessState;
assetBuyer: AssetBuyer;
web3Wrapper: Web3Wrapper;
affiliateInfo?: AffiliateInfo;
onViewTransaction: () => void;
onValidationPending: (buyQuote: BuyQuote) => void;
Expand Down Expand Up @@ -49,8 +54,11 @@ export const BuyOrderStateButtons: React.StatelessComponent<BuyOrderStateButtonP

return (
<BuyButton
accountAddress={props.accountAddress}
accountEthBalanceInWei={props.accountEthBalanceInWei}
buyQuote={props.buyQuote}
assetBuyer={props.assetBuyer}
web3Wrapper={props.web3Wrapper}
affiliateInfo={props.affiliateInfo}
onValidationPending={props.onValidationPending}
onValidationFail={props.onValidationFail}
Expand Down
2 changes: 2 additions & 0 deletions packages/instant/src/components/zero_ex_instant_provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider
asyncData.fetchAvailableAssetDatasAndDispatchToStore(this._store);
}
// tslint:disable-next-line:no-floating-promises
asyncData.fetchAccountInfoAndDispatchToStore(this._store);
// tslint:disable-next-line:no-floating-promises
asyncData.fetchCurrentBuyQuoteAndDispatchToStore(this._store);
// warm up the gas price estimator cache just in case we can't
// grab the gas price estimate when submitting the transaction
Expand Down
14 changes: 13 additions & 1 deletion packages/instant/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BigNumber } from '@0x/utils';

import { Network } from './types';
import { AccountNotReady, AccountState, Network } from './types';

export const BIG_NUMBER_ZERO = new BigNumber(0);
export const ETH_DECIMALS = 18;
Expand All @@ -22,3 +22,15 @@ export const ETHEREUM_NODE_URL_BY_NETWORK = {
[Network.Kovan]: 'https://kovan.infura.io/',
};
export const BLOCK_POLLING_INTERVAL_MS = 10000; // 10s
export const NO_ACCOUNT: AccountNotReady = {
state: AccountState.None,
};
export const LOADING_ACCOUNT: AccountNotReady = {
state: AccountState.Loading,
};
export const LOCKED_ACCOUNT: AccountNotReady = {
state: AccountState.Locked,
};
export const ERROR_ACCOUNT: AccountNotReady = {
state: AccountState.Error,
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { AssetBuyer, AssetBuyerError, BuyQuote } from '@0x/asset-buyer';
import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import * as _ from 'lodash';
import * as React from 'react';
import { connect } from 'react-redux';
Expand All @@ -7,14 +9,17 @@ import { Dispatch } from 'redux';
import { BuyOrderStateButtons } from '../components/buy_order_state_buttons';
import { Action, actions } from '../redux/actions';
import { State } from '../redux/reducer';
import { AffiliateInfo, OrderProcessState, ZeroExInstantError } from '../types';
import { AccountState, AffiliateInfo, OrderProcessState, ZeroExInstantError } from '../types';
import { errorFlasher } from '../util/error_flasher';
import { etherscanUtil } from '../util/etherscan';

interface ConnectedState {
accountAddress?: string;
accountEthBalanceInWei?: BigNumber;
buyQuote?: BuyQuote;
buyOrderProcessingState: OrderProcessState;
assetBuyer: AssetBuyer;
web3Wrapper: Web3Wrapper;
affiliateInfo?: AffiliateInfo;
onViewTransaction: () => void;
}
Expand All @@ -31,9 +36,16 @@ interface ConnectedDispatch {
export interface SelectedAssetBuyOrderStateButtons {}
const mapStateToProps = (state: State, _ownProps: SelectedAssetBuyOrderStateButtons): ConnectedState => {
const assetBuyer = state.providerState.assetBuyer;
const web3Wrapper = state.providerState.web3Wrapper;
const account = state.providerState.account;
const accountAddress = account.state === AccountState.Ready ? account.address : undefined;
const accountEthBalanceInWei = account.state === AccountState.Ready ? account.ethBalanceInWei : undefined;
return {
accountAddress,
accountEthBalanceInWei,
buyOrderProcessingState: state.buyOrderState.processState,
assetBuyer,
web3Wrapper,
buyQuote: state.latestBuyQuote,
affiliateInfo: state.affiliateInfo,
onViewTransaction: () => {
Expand Down
13 changes: 12 additions & 1 deletion packages/instant/src/redux/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { BuyQuote } from '@0x/asset-buyer';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';

import { ActionsUnion, Asset } from '../types';
import { ActionsUnion, AddressAndEthBalanceInWei, Asset } from '../types';

export interface PlainAction<T extends string> {
type: T;
Expand All @@ -21,6 +21,11 @@ function createAction<T extends string, P>(type: T, data?: P): PlainAction<T> |
}

export enum ActionTypes {
SET_ACCOUNT_STATE_LOADING = 'SET_ACCOUNT_STATE_LOADING',
SET_ACCOUNT_STATE_LOCKED = 'SET_ACCOUNT_STATE_LOCKED',
SET_ACCOUNT_STATE_ERROR = 'SET_ACCOUNT_STATE_ERROR',
SET_ACCOUNT_STATE_READY = 'SET_ACCOUNT_STATE_READY',
UPDATE_ACCOUNT_ETH_BALANCE = 'UPDATE_ACCOUNT_ETH_BALANCE',
UPDATE_ETH_USD_PRICE = 'UPDATE_ETH_USD_PRICE',
UPDATE_SELECTED_ASSET_AMOUNT = 'UPDATE_SELECTED_ASSET_AMOUNT',
SET_BUY_ORDER_STATE_NONE = 'SET_BUY_ORDER_STATE_NONE',
Expand All @@ -40,6 +45,12 @@ export enum ActionTypes {
}

export const actions = {
setAccountStateLoading: () => createAction(ActionTypes.SET_ACCOUNT_STATE_LOADING),
setAccountStateLocked: () => createAction(ActionTypes.SET_ACCOUNT_STATE_LOCKED),
setAccountStateError: () => createAction(ActionTypes.SET_ACCOUNT_STATE_ERROR),
setAccountStateReady: (address: string) => createAction(ActionTypes.SET_ACCOUNT_STATE_READY, address),
updateAccountEthBalance: (addressAndBalance: AddressAndEthBalanceInWei) =>
createAction(ActionTypes.UPDATE_ACCOUNT_ETH_BALANCE, addressAndBalance),
updateEthUsdPrice: (price?: BigNumber) => createAction(ActionTypes.UPDATE_ETH_USD_PRICE, price),
updateSelectedAssetAmount: (amount?: BigNumber) => createAction(ActionTypes.UPDATE_SELECTED_ASSET_AMOUNT, amount),
setBuyOrderStateNone: () => createAction(ActionTypes.SET_BUY_ORDER_STATE_NONE),
Expand Down
40 changes: 39 additions & 1 deletion packages/instant/src/redux/async_data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { AssetProxyId } from '@0x/types';
import * as _ from 'lodash';

import { BIG_NUMBER_ZERO } from '../constants';
import { ERC20Asset } from '../types';
import { AccountState, ERC20Asset } from '../types';
import { assetUtils } from '../util/asset';
import { buyQuoteUpdater } from '../util/buy_quote_updater';
import { coinbaseApi } from '../util/coinbase_api';
Expand Down Expand Up @@ -36,6 +36,44 @@ export const asyncData = {
store.dispatch(actions.setAvailableAssets([]));
}
},
fetchAccountInfoAndDispatchToStore: async (store: Store) => {
const { providerState } = store.getState();
const web3Wrapper = providerState.web3Wrapper;
if (providerState.account.state !== AccountState.Loading) {
store.dispatch(actions.setAccountStateLoading());
}
let availableAddresses: string[];
try {
availableAddresses = await web3Wrapper.getAvailableAddressesAsync();
} catch (e) {
store.dispatch(actions.setAccountStateError());
return;
}
if (!_.isEmpty(availableAddresses)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super nit (not necessary to change) but I'd prefer for this to follow the return pattern above, i.e.

if (_.isEmpty(availableAddresses)) {
   store.dispatch(actions.setAccountStateLocked());
   return;
}

... setAccountStateReady code ..

const activeAddress = availableAddresses[0];
store.dispatch(actions.setAccountStateReady(activeAddress));
// tslint:disable-next-line:no-floating-promises
asyncData.fetchAccountBalanceAndDispatchToStore(store);
} else {
store.dispatch(actions.setAccountStateLocked());
}
},
fetchAccountBalanceAndDispatchToStore: async (store: Store) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible that we'd have a concurrency issue in which we'd dispatch a balance for an account that they have since changed? I feel like we may need a similar check here as we have for the buy quote.

Alternatively, part of me feels like balance fetching could be paired with address fetching. This could be easier to reason about -- we always get the address and the balance together, or the whole thing is considered "failed". I'm not married to this though, what do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you are correct wrt to the balance update coming late issue

const { providerState } = store.getState();
const web3Wrapper = providerState.web3Wrapper;
const account = providerState.account;
if (account.state !== AccountState.Ready) {
return;
}
try {
const address = account.address;
const ethBalanceInWei = await web3Wrapper.getBalanceInWeiAsync(address);
store.dispatch(actions.updateAccountEthBalance({ address, ethBalanceInWei }));
} catch (e) {
// leave balance as is
return;
}
},
fetchCurrentBuyQuoteAndDispatchToStore: async (store: Store) => {
const { providerState, selectedAsset, selectedAssetAmount, affiliateInfo } = store.getState();
const assetBuyer = providerState.assetBuyer;
Expand Down
43 changes: 42 additions & 1 deletion packages/instant/src/redux/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import * as _ from 'lodash';

import { ERROR_ACCOUNT, LOADING_ACCOUNT, LOCKED_ACCOUNT } from '../constants';
import { assetMetaDataMap } from '../data/asset_meta_data_map';
import {
Account,
AccountReady,
AccountState,
AffiliateInfo,
Asset,
AssetMetaData,
Expand Down Expand Up @@ -57,6 +61,32 @@ export const DEFAULT_STATE: DefaultState = {
export const createReducer = (initialState: State) => {
const reducer = (state: State = initialState, action: Action): State => {
switch (action.type) {
case ActionTypes.SET_ACCOUNT_STATE_LOADING:
return reduceStateWithAccount(state, LOADING_ACCOUNT);
case ActionTypes.SET_ACCOUNT_STATE_LOCKED:
return reduceStateWithAccount(state, LOCKED_ACCOUNT);
case ActionTypes.SET_ACCOUNT_STATE_ERROR:
return reduceStateWithAccount(state, ERROR_ACCOUNT);
case ActionTypes.SET_ACCOUNT_STATE_READY: {
const account: AccountReady = {
state: AccountState.Ready,
address: action.data,
};
return reduceStateWithAccount(state, account);
}
case ActionTypes.UPDATE_ACCOUNT_ETH_BALANCE: {
const { address, ethBalanceInWei } = action.data;
const currentAccount = state.providerState.account;
if (currentAccount.state !== AccountState.Ready || currentAccount.address !== address) {
return state;
} else {
const newAccount: AccountReady = {
...currentAccount,
ethBalanceInWei,
};
return reduceStateWithAccount(state, newAccount);
}
}
case ActionTypes.UPDATE_ETH_USD_PRICE:
return {
...state,
Expand All @@ -80,7 +110,6 @@ export const createReducer = (initialState: State) => {
} else {
return state;
}

case ActionTypes.SET_QUOTE_REQUEST_STATE_PENDING:
return {
...state,
Expand Down Expand Up @@ -191,6 +220,18 @@ export const createReducer = (initialState: State) => {
return reducer;
};

const reduceStateWithAccount = (state: State, account: Account) => {
const oldProviderState = state.providerState;
const newProviderState: ProviderState = {
...oldProviderState,
account,
};
return {
...state,
providerState: newProviderState,
};
};

const doesBuyQuoteMatchState = (buyQuote: BuyQuote, state: State): boolean => {
const selectedAssetIfExists = state.selectedAsset;
const selectedAssetAmountIfExists = state.selectedAssetAmount;
Expand Down
5 changes: 5 additions & 0 deletions packages/instant/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,8 @@ export interface AccountNotReady {
export type Account = AccountReady | AccountNotReady;

export type OrderSource = string | SignedOrder[];

export interface AddressAndEthBalanceInWei {
address: string;
ethBalanceInWei: BigNumber;
}
13 changes: 0 additions & 13 deletions packages/instant/src/util/balance.ts

This file was deleted.

Loading