diff options
-rw-r--r-- | packages/instant/src/components/zero_ex_instant_provider.tsx | 33 | ||||
-rw-r--r-- | packages/instant/src/constants.ts | 5 | ||||
-rw-r--r-- | packages/instant/src/containers/selected_erc20_asset_amount_input.ts | 4 | ||||
-rw-r--r-- | packages/instant/src/redux/actions.ts | 2 | ||||
-rw-r--r-- | packages/instant/src/redux/async_data.ts | 23 | ||||
-rw-r--r-- | packages/instant/src/redux/reducer.ts | 4 | ||||
-rw-r--r-- | packages/instant/src/types.ts | 7 | ||||
-rw-r--r-- | packages/instant/src/util/buy_quote_updater.ts | 7 | ||||
-rw-r--r-- | packages/instant/src/util/heartbeater.ts | 35 | ||||
-rw-r--r-- | packages/instant/src/util/heartbeater_factory.ts | 22 |
10 files changed, 115 insertions, 27 deletions
diff --git a/packages/instant/src/components/zero_ex_instant_provider.tsx b/packages/instant/src/components/zero_ex_instant_provider.tsx index cceb44377..411f118cc 100644 --- a/packages/instant/src/components/zero_ex_instant_provider.tsx +++ b/packages/instant/src/components/zero_ex_instant_provider.tsx @@ -5,15 +5,18 @@ import * as _ from 'lodash'; import * as React from 'react'; import { Provider as ReduxProvider } from 'react-redux'; +import { ACCOUNT_UPDATE_INTERVAL_TIME_MS, BUY_QUOTE_UPDATE_INTERVAL_TIME_MS } from '../constants'; import { SelectedAssetThemeProvider } from '../containers/selected_asset_theme_provider'; import { asyncData } from '../redux/async_data'; import { DEFAULT_STATE, DefaultState, State } from '../redux/reducer'; import { store, Store } from '../redux/store'; import { fonts } from '../style/fonts'; -import { AffiliateInfo, AssetMetaData, Network, OrderSource } from '../types'; +import { AccountState, AffiliateInfo, AssetMetaData, Network, OrderSource } from '../types'; import { assetUtils } from '../util/asset'; import { errorFlasher } from '../util/error_flasher'; import { gasPriceEstimator } from '../util/gas_price_estimator'; +import { Heartbeater } from '../util/heartbeater'; +import { generateAccountHeartbeater, generateBuyQuoteHeartbeater } from '../util/heartbeater_factory'; import { providerStateFactory } from '../util/provider_state_factory'; fonts.include(); @@ -37,6 +40,9 @@ export interface ZeroExInstantProviderOptionalProps { export class ZeroExInstantProvider extends React.Component<ZeroExInstantProviderProps> { private readonly _store: Store; + private _accountUpdateHeartbeat?: Heartbeater; + private _buyQuoteHeartbeat?: Heartbeater; + // TODO(fragosti): Write tests for this beast once we inject a provider. private static _mergeDefaultStateWithProps( props: ZeroExInstantProviderProps, @@ -92,10 +98,21 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider // tslint:disable-next-line:no-floating-promises asyncData.fetchAvailableAssetDatasAndDispatchToStore(this._store); } + if (state.providerState.account.state !== AccountState.None) { + this._accountUpdateHeartbeat = generateAccountHeartbeater({ + store: this._store, + shouldPerformImmediatelyOnStart: true, + }); + this._accountUpdateHeartbeat.start(ACCOUNT_UPDATE_INTERVAL_TIME_MS); + } + + this._buyQuoteHeartbeat = generateBuyQuoteHeartbeater({ + store: this._store, + shouldPerformImmediatelyOnStart: false, + }); + this._buyQuoteHeartbeat.start(BUY_QUOTE_UPDATE_INTERVAL_TIME_MS); // tslint:disable-next-line:no-floating-promises - asyncData.fetchAccountInfoAndDispatchToStore(this._store); - // tslint:disable-next-line:no-floating-promises - asyncData.fetchCurrentBuyQuoteAndDispatchToStore(this._store); + asyncData.fetchCurrentBuyQuoteAndDispatchToStore({ store: this._store, shouldSetPending: true }); // warm up the gas price estimator cache just in case we can't // grab the gas price estimate when submitting the transaction // tslint:disable-next-line:no-floating-promises @@ -103,6 +120,14 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider // tslint:disable-next-line:no-floating-promises this._flashErrorIfWrongNetwork(); } + public componentWillUnmount(): void { + if (this._accountUpdateHeartbeat) { + this._accountUpdateHeartbeat.stop(); + } + if (this._buyQuoteHeartbeat) { + this._buyQuoteHeartbeat.stop(); + } + } public render(): React.ReactNode { return ( <ReduxProvider store={this._store}> diff --git a/packages/instant/src/constants.ts b/packages/instant/src/constants.ts index 4db73845b..deab23a98 100644 --- a/packages/instant/src/constants.ts +++ b/packages/instant/src/constants.ts @@ -11,6 +11,8 @@ export const WEB_3_WRAPPER_TRANSACTION_FAILED_ERROR_MSG_PREFIX = 'Transaction fa export const GWEI_IN_WEI = new BigNumber(1000000000); export const ONE_SECOND_MS = 1000; export const ONE_MINUTE_MS = ONE_SECOND_MS * 60; +export const ACCOUNT_UPDATE_INTERVAL_TIME_MS = ONE_SECOND_MS * 5; +export const BUY_QUOTE_UPDATE_INTERVAL_TIME_MS = ONE_SECOND_MS * 15; export const DEFAULT_GAS_PRICE = GWEI_IN_WEI.mul(6); export const DEFAULT_ESTIMATED_TRANSACTION_TIME_MS = ONE_MINUTE_MS * 2; export const ETH_GAS_STATION_API_BASE_URL = 'https://ethgasstation.info'; @@ -35,6 +37,3 @@ export const LOADING_ACCOUNT: AccountNotReady = { export const LOCKED_ACCOUNT: AccountNotReady = { state: AccountState.Locked, }; -export const ERROR_ACCOUNT: AccountNotReady = { - state: AccountState.Error, -}; diff --git a/packages/instant/src/containers/selected_erc20_asset_amount_input.ts b/packages/instant/src/containers/selected_erc20_asset_amount_input.ts index c550aef04..93ff3db70 100644 --- a/packages/instant/src/containers/selected_erc20_asset_amount_input.ts +++ b/packages/instant/src/containers/selected_erc20_asset_amount_input.ts @@ -69,7 +69,7 @@ const mapStateToProps = (state: State, _ownProps: SelectedERC20AssetAmountInputP const debouncedUpdateBuyQuoteAsync = _.debounce(buyQuoteUpdater.updateBuyQuoteAsync.bind(buyQuoteUpdater), 200, { trailing: true, -}); +}) as typeof buyQuoteUpdater.updateBuyQuoteAsync; const mapDispatchToProps = ( dispatch: Dispatch<Action>, @@ -87,7 +87,7 @@ const mapDispatchToProps = ( // even if it's debounced, give them the illusion it's loading dispatch(actions.setQuoteRequestStatePending()); // tslint:disable-next-line:no-floating-promises - debouncedUpdateBuyQuoteAsync(assetBuyer, dispatch, asset, value, affiliateInfo); + debouncedUpdateBuyQuoteAsync(assetBuyer, dispatch, asset, value, true, affiliateInfo); } }, }); diff --git a/packages/instant/src/redux/actions.ts b/packages/instant/src/redux/actions.ts index a899da23c..0891170b4 100644 --- a/packages/instant/src/redux/actions.ts +++ b/packages/instant/src/redux/actions.ts @@ -23,7 +23,6 @@ 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', @@ -49,7 +48,6 @@ 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), diff --git a/packages/instant/src/redux/async_data.ts b/packages/instant/src/redux/async_data.ts index a8f632009..b920ac914 100644 --- a/packages/instant/src/redux/async_data.ts +++ b/packages/instant/src/redux/async_data.ts @@ -2,7 +2,7 @@ import { AssetProxyId } from '@0x/types'; import * as _ from 'lodash'; import { BIG_NUMBER_ZERO } from '../constants'; -import { AccountState, ERC20Asset } from '../types'; +import { AccountState, ERC20Asset, OrderProcessState } from '../types'; import { assetUtils } from '../util/asset'; import { buyQuoteUpdater } from '../util/buy_quote_updater'; import { coinbaseApi } from '../util/coinbase_api'; @@ -36,17 +36,23 @@ export const asyncData = { store.dispatch(actions.setAvailableAssets([])); } }, - fetchAccountInfoAndDispatchToStore: async (store: Store) => { + fetchAccountInfoAndDispatchToStore: async (options: { store: Store; shouldSetToLoading: boolean }) => { + const { store, shouldSetToLoading } = options; const { providerState } = store.getState(); const web3Wrapper = providerState.web3Wrapper; - if (providerState.account.state !== AccountState.Loading) { + const provider = providerState.provider; + if (shouldSetToLoading && providerState.account.state !== AccountState.Loading) { store.dispatch(actions.setAccountStateLoading()); } let availableAddresses: string[]; try { - availableAddresses = await web3Wrapper.getAvailableAddressesAsync(); + // TODO(bmillman): Add support at the web3Wrapper level for calling `eth_requestAccounts` instead of calling enable here + const isPrivacyModeEnabled = !_.isUndefined((provider as any).enable); + availableAddresses = isPrivacyModeEnabled + ? await (provider as any).enable() + : await web3Wrapper.getAvailableAddressesAsync(); } catch (e) { - store.dispatch(actions.setAccountStateError()); + store.dispatch(actions.setAccountStateLocked()); return; } if (!_.isEmpty(availableAddresses)) { @@ -74,12 +80,14 @@ export const asyncData = { return; } }, - fetchCurrentBuyQuoteAndDispatchToStore: async (store: Store) => { - const { providerState, selectedAsset, selectedAssetAmount, affiliateInfo } = store.getState(); + fetchCurrentBuyQuoteAndDispatchToStore: async (options: { store: Store; shouldSetPending: boolean }) => { + const { store, shouldSetPending } = options; + const { buyOrderState, providerState, selectedAsset, selectedAssetAmount, affiliateInfo } = store.getState(); const assetBuyer = providerState.assetBuyer; if ( !_.isUndefined(selectedAssetAmount) && !_.isUndefined(selectedAsset) && + buyOrderState.processState === OrderProcessState.None && selectedAsset.metaData.assetProxyId === AssetProxyId.ERC20 ) { await buyQuoteUpdater.updateBuyQuoteAsync( @@ -87,6 +95,7 @@ export const asyncData = { store.dispatch, selectedAsset as ERC20Asset, selectedAssetAmount, + shouldSetPending, affiliateInfo, ); } diff --git a/packages/instant/src/redux/reducer.ts b/packages/instant/src/redux/reducer.ts index a542979cb..4688250bc 100644 --- a/packages/instant/src/redux/reducer.ts +++ b/packages/instant/src/redux/reducer.ts @@ -4,7 +4,7 @@ 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 { LOADING_ACCOUNT, LOCKED_ACCOUNT } from '../constants'; import { assetMetaDataMap } from '../data/asset_meta_data_map'; import { Account, @@ -72,8 +72,6 @@ export const createReducer = (initialState: State) => { 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, diff --git a/packages/instant/src/types.ts b/packages/instant/src/types.ts index 0377f57d5..b6f449f38 100644 --- a/packages/instant/src/types.ts +++ b/packages/instant/src/types.ts @@ -102,11 +102,10 @@ export interface ProviderState { } export enum AccountState { + None = 'NONE,', Loading = 'LOADING', Ready = 'READY', - Locked = 'LOCKED', // TODO(bmillman): break this up into locked / privacy mode enabled - Error = 'ERROR', - None = 'NONE,', + Locked = 'LOCKED', } export interface AccountReady { @@ -115,7 +114,7 @@ export interface AccountReady { ethBalanceInWei?: BigNumber; } export interface AccountNotReady { - state: AccountState.None | AccountState.Loading | AccountState.Locked | AccountState.Error; + state: AccountState.None | AccountState.Loading | AccountState.Locked; } export type Account = AccountReady | AccountNotReady; diff --git a/packages/instant/src/util/buy_quote_updater.ts b/packages/instant/src/util/buy_quote_updater.ts index e697d3ef7..c33e28f1c 100644 --- a/packages/instant/src/util/buy_quote_updater.ts +++ b/packages/instant/src/util/buy_quote_updater.ts @@ -16,12 +16,15 @@ export const buyQuoteUpdater = { dispatch: Dispatch<Action>, asset: ERC20Asset, assetAmount: BigNumber, + setPending = true, affiliateInfo?: AffiliateInfo, ): Promise<void> => { // get a new buy quote. const baseUnitValue = Web3Wrapper.toBaseUnitAmount(assetAmount, asset.metaData.decimals); - // mark quote as pending - dispatch(actions.setQuoteRequestStatePending()); + if (setPending) { + // mark quote as pending + dispatch(actions.setQuoteRequestStatePending()); + } const feePercentage = oc(affiliateInfo).feePercentage(); let newBuyQuote: BuyQuote | undefined; try { diff --git a/packages/instant/src/util/heartbeater.ts b/packages/instant/src/util/heartbeater.ts new file mode 100644 index 000000000..e700d489e --- /dev/null +++ b/packages/instant/src/util/heartbeater.ts @@ -0,0 +1,35 @@ +import { intervalUtils } from '@0x/utils'; +import * as _ from 'lodash'; + +type HeartbeatableFunction = () => Promise<void>; +export class Heartbeater { + private _intervalId?: NodeJS.Timer; + private readonly _performImmediatelyOnStart: boolean; + private readonly _performFunction: HeartbeatableFunction; + + public constructor(performingFunctionAsync: HeartbeatableFunction, performImmediatelyOnStart: boolean) { + this._performFunction = performingFunctionAsync; + this._performImmediatelyOnStart = performImmediatelyOnStart; + } + + public start(intervalTimeMs: number): void { + if (!_.isUndefined(this._intervalId)) { + throw new Error('Heartbeat is running, please stop before restarting'); + } + + if (this._performImmediatelyOnStart) { + // tslint:disable-next-line:no-floating-promises + this._performFunction(); + } + + // tslint:disable-next-line:no-unbound-method + this._intervalId = intervalUtils.setAsyncExcludingInterval(this._performFunction, intervalTimeMs, _.noop); + } + + public stop(): void { + if (this._intervalId) { + intervalUtils.clearInterval(this._intervalId); + } + this._intervalId = undefined; + } +} diff --git a/packages/instant/src/util/heartbeater_factory.ts b/packages/instant/src/util/heartbeater_factory.ts new file mode 100644 index 000000000..96a8ac4e6 --- /dev/null +++ b/packages/instant/src/util/heartbeater_factory.ts @@ -0,0 +1,22 @@ +import { asyncData } from '../redux/async_data'; +import { Store } from '../redux/store'; + +import { Heartbeater } from './heartbeater'; + +export interface HeartbeatFactoryOptions { + store: Store; + shouldPerformImmediatelyOnStart: boolean; +} +export const generateAccountHeartbeater = (options: HeartbeatFactoryOptions): Heartbeater => { + const { store, shouldPerformImmediatelyOnStart } = options; + return new Heartbeater(async () => { + await asyncData.fetchAccountInfoAndDispatchToStore({ store, shouldSetToLoading: false }); + }, shouldPerformImmediatelyOnStart); +}; + +export const generateBuyQuoteHeartbeater = (options: HeartbeatFactoryOptions): Heartbeater => { + const { store, shouldPerformImmediatelyOnStart } = options; + return new Heartbeater(async () => { + await asyncData.fetchCurrentBuyQuoteAndDispatchToStore({ store, shouldSetPending: false }); + }, shouldPerformImmediatelyOnStart); +}; |