diff options
Diffstat (limited to 'packages/instant/src')
17 files changed, 421 insertions, 276 deletions
diff --git a/packages/instant/src/components/buy_button.tsx b/packages/instant/src/components/buy_button.tsx index e6d50de96..515cd18e9 100644 --- a/packages/instant/src/components/buy_button.tsx +++ b/packages/instant/src/components/buy_button.tsx @@ -17,7 +17,7 @@ import { Text } from './ui/text'; export interface BuyButtonProps { buyQuote?: BuyQuote; - assetBuyer?: AssetBuyer; + assetBuyer: AssetBuyer; affiliateInfo?: AffiliateInfo; onValidationPending: (buyQuote: BuyQuote) => void; onValidationFail: (buyQuote: BuyQuote, errorMessage: AssetBuyerError | ZeroExInstantError) => void; @@ -34,7 +34,7 @@ export class BuyButton extends React.Component<BuyButtonProps> { onBuyFailure: util.boundNoop, }; public render(): React.ReactNode { - const shouldDisableButton = _.isUndefined(this.props.buyQuote) || _.isUndefined(this.props.assetBuyer); + const shouldDisableButton = _.isUndefined(this.props.buyQuote); return ( <Button width="100%" onClick={this._handleClick} isDisabled={shouldDisableButton}> <Text fontColor={ColorOption.white} fontWeight={600} fontSize="20px"> @@ -46,11 +46,13 @@ 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) || _.isUndefined(assetBuyer)) { + if (_.isUndefined(buyQuote)) { 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); diff --git a/packages/instant/src/components/buy_order_progress.tsx b/packages/instant/src/components/buy_order_progress.tsx index 7fe4e77e9..bc7319423 100644 --- a/packages/instant/src/components/buy_order_progress.tsx +++ b/packages/instant/src/components/buy_order_progress.tsx @@ -14,12 +14,12 @@ export const BuyOrderProgress: React.StatelessComponent<BuyOrderProgressProps> = const { buyOrderState } = props; if ( - buyOrderState.processState === OrderProcessState.PROCESSING || - buyOrderState.processState === OrderProcessState.SUCCESS || - buyOrderState.processState === OrderProcessState.FAILURE + buyOrderState.processState === OrderProcessState.Processing || + buyOrderState.processState === OrderProcessState.Success || + buyOrderState.processState === OrderProcessState.Failure ) { const progress = buyOrderState.progress; - const hasEnded = buyOrderState.processState !== OrderProcessState.PROCESSING; + const hasEnded = buyOrderState.processState !== OrderProcessState.Processing; const expectedTimeMs = progress.expectedEndTimeUnix - progress.startTimeUnix; return ( <Container padding="20px 20px 0px 20px" width="100%"> diff --git a/packages/instant/src/components/buy_order_state_buttons.tsx b/packages/instant/src/components/buy_order_state_buttons.tsx index 0d54d3187..3d0ede7b1 100644 --- a/packages/instant/src/components/buy_order_state_buttons.tsx +++ b/packages/instant/src/components/buy_order_state_buttons.tsx @@ -15,7 +15,7 @@ import { Text } from './ui/text'; export interface BuyOrderStateButtonProps { buyQuote?: BuyQuote; buyOrderProcessingState: OrderProcessState; - assetBuyer?: AssetBuyer; + assetBuyer: AssetBuyer; affiliateInfo?: AffiliateInfo; onViewTransaction: () => void; onValidationPending: (buyQuote: BuyQuote) => void; @@ -28,7 +28,7 @@ export interface BuyOrderStateButtonProps { } export const BuyOrderStateButtons: React.StatelessComponent<BuyOrderStateButtonProps> = props => { - if (props.buyOrderProcessingState === OrderProcessState.FAILURE) { + if (props.buyOrderProcessingState === OrderProcessState.Failure) { return ( <Flex justify="space-between"> <Button width="48%" onClick={props.onRetry}> @@ -42,11 +42,11 @@ export const BuyOrderStateButtons: React.StatelessComponent<BuyOrderStateButtonP </Flex> ); } else if ( - props.buyOrderProcessingState === OrderProcessState.SUCCESS || - props.buyOrderProcessingState === OrderProcessState.PROCESSING + props.buyOrderProcessingState === OrderProcessState.Success || + props.buyOrderProcessingState === OrderProcessState.Processing ) { return <SecondaryButton onClick={props.onViewTransaction}>View Transaction</SecondaryButton>; - } else if (props.buyOrderProcessingState === OrderProcessState.VALIDATING) { + } else if (props.buyOrderProcessingState === OrderProcessState.Validating) { return <PlacingOrderButton />; } diff --git a/packages/instant/src/components/instant_heading.tsx b/packages/instant/src/components/instant_heading.tsx index 6d87bc292..b07776b2c 100644 --- a/packages/instant/src/components/instant_heading.tsx +++ b/packages/instant/src/components/instant_heading.tsx @@ -77,11 +77,11 @@ export class InstantHeading extends React.Component<InstantHeadingProps, {}> { private _renderIcon(): React.ReactNode { const processState = this.props.buyOrderState.processState; - if (processState === OrderProcessState.FAILURE) { + if (processState === OrderProcessState.Failure) { return <Icon icon="failed" width={ICON_WIDTH} height={ICON_HEIGHT} color={ICON_COLOR} />; - } else if (processState === OrderProcessState.PROCESSING) { + } else if (processState === OrderProcessState.Processing) { return <Spinner widthPx={ICON_HEIGHT} heightPx={ICON_HEIGHT} />; - } else if (processState === OrderProcessState.SUCCESS) { + } else if (processState === OrderProcessState.Success) { return <Icon icon="success" width={ICON_WIDTH} height={ICON_HEIGHT} color={ICON_COLOR} />; } return undefined; @@ -89,11 +89,11 @@ export class InstantHeading extends React.Component<InstantHeadingProps, {}> { private _renderTopText(): React.ReactNode { const processState = this.props.buyOrderState.processState; - if (processState === OrderProcessState.FAILURE) { + if (processState === OrderProcessState.Failure) { return 'Order failed'; - } else if (processState === OrderProcessState.PROCESSING) { + } else if (processState === OrderProcessState.Processing) { return 'Processing Order...'; - } else if (processState === OrderProcessState.SUCCESS) { + } else if (processState === OrderProcessState.Success) { return 'Tokens received!'; } @@ -101,7 +101,7 @@ export class InstantHeading extends React.Component<InstantHeadingProps, {}> { } private _renderPlaceholderOrAmount(amountFunction: () => React.ReactNode): React.ReactNode { - if (this.props.quoteRequestState === AsyncProcessState.PENDING) { + if (this.props.quoteRequestState === AsyncProcessState.Pending) { return <AmountPlaceholder isPulsating={true} color={PLACEHOLDER_COLOR} />; } if (_.isUndefined(this.props.selectedAssetAmount)) { diff --git a/packages/instant/src/components/zero_ex_instant_provider.tsx b/packages/instant/src/components/zero_ex_instant_provider.tsx index 0b9408329..1fb5cf64f 100644 --- a/packages/instant/src/components/zero_ex_instant_provider.tsx +++ b/packages/instant/src/components/zero_ex_instant_provider.tsx @@ -1,23 +1,20 @@ -import { AssetBuyer } from '@0x/asset-buyer'; -import { ObjectMap, SignedOrder } from '@0x/types'; +import { ObjectMap } from '@0x/types'; import { BigNumber } from '@0x/utils'; -import { Web3Wrapper } from '@0x/web3-wrapper'; import { Provider } from 'ethereum-types'; import * as _ from 'lodash'; import * as React from 'react'; import { Provider as ReduxProvider } from 'react-redux'; -import { oc } from 'ts-optchain'; import { SelectedAssetThemeProvider } from '../containers/selected_asset_theme_provider'; import { asyncData } from '../redux/async_data'; -import { INITIAL_STATE, State } from '../redux/reducer'; +import { DEFAULT_STATE, DefaultState, State } from '../redux/reducer'; import { store, Store } from '../redux/store'; import { fonts } from '../style/fonts'; -import { AffiliateInfo, AssetMetaData, Network } from '../types'; +import { 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 { getInjectedProvider } from '../util/injected_provider'; +import { providerStateFactory } from '../util/provider_state_factory'; fonts.include(); @@ -25,7 +22,7 @@ export type ZeroExInstantProviderProps = ZeroExInstantProviderRequiredProps & Partial<ZeroExInstantProviderOptionalProps>; export interface ZeroExInstantProviderRequiredProps { - orderSource: string | SignedOrder[]; + orderSource: OrderSource; } export interface ZeroExInstantProviderOptionalProps { @@ -41,30 +38,27 @@ export interface ZeroExInstantProviderOptionalProps { export class ZeroExInstantProvider extends React.Component<ZeroExInstantProviderProps> { private readonly _store: Store; // TODO(fragosti): Write tests for this beast once we inject a provider. - private static _mergeInitialStateWithProps(props: ZeroExInstantProviderProps, state: State = INITIAL_STATE): State { - const networkId = props.networkId || state.network; - // TODO: Proper wallet connect flow - const provider = props.provider || getInjectedProvider(); - const assetBuyerOptions = { + private static _mergeDefaultStateWithProps( + props: ZeroExInstantProviderProps, + defaultState: DefaultState = DEFAULT_STATE, + ): State { + // use the networkId passed in with the props, otherwise default to that of the default state (1, mainnet) + const networkId = props.networkId || defaultState.network; + // construct the ProviderState + const providerState = providerStateFactory.getInitialProviderState( + props.orderSource, networkId, - }; - let assetBuyer; - if (_.isString(props.orderSource)) { - assetBuyer = AssetBuyer.getAssetBuyerForStandardRelayerAPIUrl( - provider, - props.orderSource, - assetBuyerOptions, - ); - } else { - assetBuyer = AssetBuyer.getAssetBuyerForProvidedOrders(provider, props.orderSource, assetBuyerOptions); - } + props.provider, + ); + // merge the additional additionalAssetMetaDataMap with our default map const completeAssetMetaDataMap = { ...props.additionalAssetMetaDataMap, - ...state.assetMetaDataMap, + ...defaultState.assetMetaDataMap, }; + // construct the final state const storeStateFromProps: State = { - ...state, - assetBuyer, + ...defaultState, + providerState, network: networkId, selectedAsset: _.isUndefined(props.defaultSelectedAssetData) ? undefined @@ -74,7 +68,7 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider networkId, ), selectedAssetAmount: _.isUndefined(props.defaultAssetBuyAmount) - ? state.selectedAssetAmount + ? undefined : new BigNumber(props.defaultAssetBuyAmount), availableAssets: _.isUndefined(props.availableAssetDatas) ? undefined @@ -86,10 +80,9 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider } constructor(props: ZeroExInstantProviderProps) { super(props); - const initialAppState = ZeroExInstantProvider._mergeInitialStateWithProps(this.props, INITIAL_STATE); + const initialAppState = ZeroExInstantProvider._mergeDefaultStateWithProps(this.props); this._store = store.create(initialAppState); } - public componentDidMount(): void { const state = this._store.getState(); // tslint:disable-next-line:no-floating-promises @@ -108,7 +101,6 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider // tslint:disable-next-line:no-floating-promises this._flashErrorIfWrongNetwork(); } - public render(): React.ReactNode { return ( <ReduxProvider store={this._store}> @@ -116,19 +108,15 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider </ReduxProvider> ); } - private readonly _flashErrorIfWrongNetwork = async (): Promise<void> => { const msToShowError = 30000; // 30 seconds - const network = this._store.getState().network; - const assetBuyerIfExists = this._store.getState().assetBuyer; - const providerIfExists = oc(assetBuyerIfExists).provider(); - if (!_.isUndefined(providerIfExists)) { - const web3Wrapper = new Web3Wrapper(providerIfExists); - const networkOfProvider = await web3Wrapper.getNetworkIdAsync(); - if (network !== networkOfProvider) { - const errorMessage = `Wrong network detected. Try switching to ${Network[network]}.`; - errorFlasher.flashNewErrorMessage(this._store.dispatch, errorMessage, msToShowError); - } + const state = this._store.getState(); + const network = state.network; + const web3Wrapper = state.providerState.web3Wrapper; + const networkOfProvider = await web3Wrapper.getNetworkIdAsync(); + if (network !== networkOfProvider) { + const errorMessage = `Wrong network detected. Try switching to ${Network[network]}.`; + errorFlasher.flashNewErrorMessage(this._store.dispatch, errorMessage, msToShowError); } }; } diff --git a/packages/instant/src/constants.ts b/packages/instant/src/constants.ts index 15105b65c..00521b56e 100644 --- a/packages/instant/src/constants.ts +++ b/packages/instant/src/constants.ts @@ -1,4 +1,7 @@ import { BigNumber } from '@0x/utils'; + +import { Network } from './types'; + export const BIG_NUMBER_ZERO = new BigNumber(0); export const ETH_DECIMALS = 18; export const DEFAULT_ZERO_EX_CONTAINER_SELECTOR = '#zeroExInstantContainer'; @@ -13,3 +16,8 @@ export const ETH_GAS_STATION_API_BASE_URL = 'https://ethgasstation.info'; export const COINBASE_API_BASE_URL = 'https://api.coinbase.com/v2'; export const PROGRESS_STALL_AT_WIDTH = '95%'; export const PROGRESS_FINISH_ANIMATION_TIME_MS = 200; +export const ETHEREUM_NODE_URL_BY_NETWORK = { + [Network.Mainnet]: 'https://mainnet.infura.io/', + [Network.Kovan]: 'https://kovan.infura.io/', +}; +export const BLOCK_POLLING_INTERVAL_MS = 10000; // 10s diff --git a/packages/instant/src/containers/latest_buy_quote_order_details.ts b/packages/instant/src/containers/latest_buy_quote_order_details.ts index 092aaaf20..2b59ed3ae 100644 --- a/packages/instant/src/containers/latest_buy_quote_order_details.ts +++ b/packages/instant/src/containers/latest_buy_quote_order_details.ts @@ -22,7 +22,7 @@ const mapStateToProps = (state: State, _ownProps: LatestBuyQuoteOrderDetailsProp // use the worst case quote info buyQuoteInfo: oc(state).latestBuyQuote.worstCaseQuoteInfo(), ethUsdPrice: state.ethUsdPrice, - isLoading: state.quoteRequestState === AsyncProcessState.PENDING, + isLoading: state.quoteRequestState === AsyncProcessState.Pending, }); export const LatestBuyQuoteOrderDetails: React.ComponentClass<LatestBuyQuoteOrderDetailsProps> = connect( diff --git a/packages/instant/src/containers/selected_asset_buy_order_state_buttons.ts b/packages/instant/src/containers/selected_asset_buy_order_state_buttons.ts index 72d99f844..c3a5e88b9 100644 --- a/packages/instant/src/containers/selected_asset_buy_order_state_buttons.ts +++ b/packages/instant/src/containers/selected_asset_buy_order_state_buttons.ts @@ -14,7 +14,7 @@ import { etherscanUtil } from '../util/etherscan'; interface ConnectedState { buyQuote?: BuyQuote; buyOrderProcessingState: OrderProcessState; - assetBuyer?: AssetBuyer; + assetBuyer: AssetBuyer; affiliateInfo?: AffiliateInfo; onViewTransaction: () => void; } @@ -29,29 +29,31 @@ interface ConnectedDispatch { onValidationFail: (buyQuote: BuyQuote, errorMessage: AssetBuyerError | ZeroExInstantError) => void; } export interface SelectedAssetBuyOrderStateButtons {} -const mapStateToProps = (state: State, _ownProps: SelectedAssetBuyOrderStateButtons): ConnectedState => ({ - buyOrderProcessingState: state.buyOrderState.processState, - assetBuyer: state.assetBuyer, - buyQuote: state.latestBuyQuote, - affiliateInfo: state.affiliateInfo, - onViewTransaction: () => { - if ( - state.assetBuyer && - (state.buyOrderState.processState === OrderProcessState.PROCESSING || - state.buyOrderState.processState === OrderProcessState.SUCCESS || - state.buyOrderState.processState === OrderProcessState.FAILURE) - ) { - const etherscanUrl = etherscanUtil.getEtherScanTxnAddressIfExists( - state.buyOrderState.txHash, - state.assetBuyer.networkId, - ); - if (etherscanUrl) { - window.open(etherscanUrl, '_blank'); - return; +const mapStateToProps = (state: State, _ownProps: SelectedAssetBuyOrderStateButtons): ConnectedState => { + const assetBuyer = state.providerState.assetBuyer; + return { + buyOrderProcessingState: state.buyOrderState.processState, + assetBuyer, + buyQuote: state.latestBuyQuote, + affiliateInfo: state.affiliateInfo, + onViewTransaction: () => { + if ( + state.buyOrderState.processState === OrderProcessState.Processing || + state.buyOrderState.processState === OrderProcessState.Success || + state.buyOrderState.processState === OrderProcessState.Failure + ) { + const etherscanUrl = etherscanUtil.getEtherScanTxnAddressIfExists( + state.buyOrderState.txHash, + assetBuyer.networkId, + ); + if (etherscanUrl) { + window.open(etherscanUrl, '_blank'); + return; + } } - } - }, -}); + }, + }; +}; const mapDispatchToProps = ( dispatch: Dispatch<Action>, 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 f7d56c564..784eb4bd0 100644 --- a/packages/instant/src/containers/selected_erc20_asset_amount_input.ts +++ b/packages/instant/src/containers/selected_erc20_asset_amount_input.ts @@ -23,7 +23,7 @@ export interface SelectedERC20AssetAmountInputProps { } interface ConnectedState { - assetBuyer?: AssetBuyer; + assetBuyer: AssetBuyer; value?: BigNumber; asset?: ERC20Asset; isDisabled: boolean; @@ -33,7 +33,7 @@ interface ConnectedState { interface ConnectedDispatch { updateBuyQuote: ( - assetBuyer?: AssetBuyer, + assetBuyer: AssetBuyer, value?: BigNumber, asset?: ERC20Asset, affiliateInfo?: AffiliateInfo, @@ -52,15 +52,16 @@ type FinalProps = ConnectedProps & SelectedERC20AssetAmountInputProps; const mapStateToProps = (state: State, _ownProps: SelectedERC20AssetAmountInputProps): ConnectedState => { const processState = state.buyOrderState.processState; - const isEnabled = processState === OrderProcessState.NONE || processState === OrderProcessState.FAILURE; + const isEnabled = processState === OrderProcessState.None || processState === OrderProcessState.Failure; const isDisabled = !isEnabled; const selectedAsset = !_.isUndefined(state.selectedAsset) && state.selectedAsset.metaData.assetProxyId === AssetProxyId.ERC20 ? (state.selectedAsset as ERC20Asset) : undefined; const numberOfAssetsAvailable = _.isUndefined(state.availableAssets) ? undefined : state.availableAssets.length; + const assetBuyer = state.providerState.assetBuyer; return { - assetBuyer: state.assetBuyer, + assetBuyer, value: state.selectedAssetAmount, asset: selectedAsset, isDisabled, @@ -128,7 +129,7 @@ const mapDispatchToProps = ( // reset our buy state dispatch(actions.setBuyOrderStateNone()); - if (!_.isUndefined(value) && value.greaterThan(0) && !_.isUndefined(asset) && !_.isUndefined(assetBuyer)) { + if (!_.isUndefined(value) && value.greaterThan(0) && !_.isUndefined(asset)) { // even if it's debounced, give them the illusion it's loading dispatch(actions.setQuoteRequestStatePending()); // tslint:disable-next-line:no-floating-promises diff --git a/packages/instant/src/redux/async_data.ts b/packages/instant/src/redux/async_data.ts index 0e05c13da..c3d190af2 100644 --- a/packages/instant/src/redux/async_data.ts +++ b/packages/instant/src/redux/async_data.ts @@ -20,18 +20,17 @@ export const asyncData = { } }, fetchAvailableAssetDatasAndDispatchToStore: async (store: Store) => { - const { assetBuyer, assetMetaDataMap, network } = store.getState(); - if (!_.isUndefined(assetBuyer)) { - try { - const assetDatas = await assetBuyer.getAvailableAssetDatasAsync(); - const assets = assetUtils.createAssetsFromAssetDatas(assetDatas, assetMetaDataMap, network); - store.dispatch(actions.setAvailableAssets(assets)); - } catch (e) { - const errorMessage = 'Could not find any assets'; - errorFlasher.flashNewErrorMessage(store.dispatch, errorMessage); - // On error, just specify that none are available - store.dispatch(actions.setAvailableAssets([])); - } + const { providerState, assetMetaDataMap, network } = store.getState(); + const assetBuyer = providerState.assetBuyer; + try { + const assetDatas = await assetBuyer.getAvailableAssetDatasAsync(); + const assets = assetUtils.createAssetsFromAssetDatas(assetDatas, assetMetaDataMap, network); + store.dispatch(actions.setAvailableAssets(assets)); + } catch (e) { + const errorMessage = 'Could not find any assets'; + errorFlasher.flashNewErrorMessage(store.dispatch, errorMessage); + // On error, just specify that none are available + store.dispatch(actions.setAvailableAssets([])); } }, }; diff --git a/packages/instant/src/redux/reducer.ts b/packages/instant/src/redux/reducer.ts index bc435d069..4a939839a 100644 --- a/packages/instant/src/redux/reducer.ts +++ b/packages/instant/src/redux/reducer.ts @@ -1,4 +1,4 @@ -import { AssetBuyer, BuyQuote } from '@0x/asset-buyer'; +import { BuyQuote } from '@0x/asset-buyer'; import { AssetProxyId, ObjectMap } from '@0x/types'; import { BigNumber } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; @@ -14,172 +14,181 @@ import { Network, OrderProcessState, OrderState, + ProviderState, } from '../types'; import { Action, ActionTypes } from './actions'; -export interface State { +// State that is required and we have defaults for, before props are passed in +export interface DefaultState { network: Network; - assetBuyer?: AssetBuyer; assetMetaDataMap: ObjectMap<AssetMetaData>; - selectedAsset?: Asset; - availableAssets?: Asset[]; - selectedAssetAmount?: BigNumber; buyOrderState: OrderState; - ethUsdPrice?: BigNumber; - latestBuyQuote?: BuyQuote; - quoteRequestState: AsyncProcessState; - latestErrorMessage?: string; latestErrorDisplayStatus: DisplayStatus; - affiliateInfo?: AffiliateInfo; + quoteRequestState: AsyncProcessState; +} + +// State that is required but needs to be derived from the props +interface PropsDerivedState { + providerState: ProviderState; } -export const INITIAL_STATE: State = { +// State that is optional +interface OptionalState { + selectedAsset: Asset; + availableAssets: Asset[]; + selectedAssetAmount: BigNumber; + ethUsdPrice: BigNumber; + latestBuyQuote: BuyQuote; + latestErrorMessage: string; + affiliateInfo: AffiliateInfo; +} + +export type State = DefaultState & PropsDerivedState & Partial<OptionalState>; + +export const DEFAULT_STATE: DefaultState = { network: Network.Mainnet, - selectedAssetAmount: undefined, - availableAssets: undefined, assetMetaDataMap, - buyOrderState: { processState: OrderProcessState.NONE }, - ethUsdPrice: undefined, - latestBuyQuote: undefined, - latestErrorMessage: undefined, + buyOrderState: { processState: OrderProcessState.None }, latestErrorDisplayStatus: DisplayStatus.Hidden, - quoteRequestState: AsyncProcessState.NONE, - affiliateInfo: undefined, + quoteRequestState: AsyncProcessState.None, }; -export const reducer = (state: State = INITIAL_STATE, action: Action): State => { - switch (action.type) { - case ActionTypes.UPDATE_ETH_USD_PRICE: - return { - ...state, - ethUsdPrice: action.data, - }; - case ActionTypes.UPDATE_SELECTED_ASSET_AMOUNT: - return { - ...state, - selectedAssetAmount: action.data, - }; - case ActionTypes.UPDATE_LATEST_BUY_QUOTE: - const newBuyQuoteIfExists = action.data; - const shouldUpdate = - _.isUndefined(newBuyQuoteIfExists) || doesBuyQuoteMatchState(newBuyQuoteIfExists, state); - if (shouldUpdate) { +export const createReducer = (initialState: State) => { + const reducer = (state: State = initialState, action: Action): State => { + switch (action.type) { + case ActionTypes.UPDATE_ETH_USD_PRICE: return { ...state, - latestBuyQuote: newBuyQuoteIfExists, - quoteRequestState: AsyncProcessState.SUCCESS, + ethUsdPrice: action.data, }; - } else { - return state; - } - - case ActionTypes.SET_QUOTE_REQUEST_STATE_PENDING: - return { - ...state, - latestBuyQuote: undefined, - quoteRequestState: AsyncProcessState.PENDING, - }; - case ActionTypes.SET_QUOTE_REQUEST_STATE_FAILURE: - return { - ...state, - latestBuyQuote: undefined, - quoteRequestState: AsyncProcessState.FAILURE, - }; - case ActionTypes.SET_BUY_ORDER_STATE_NONE: - return { - ...state, - buyOrderState: { processState: OrderProcessState.NONE }, - }; - case ActionTypes.SET_BUY_ORDER_STATE_VALIDATING: - return { - ...state, - buyOrderState: { processState: OrderProcessState.VALIDATING }, - }; - case ActionTypes.SET_BUY_ORDER_STATE_PROCESSING: - const processingData = action.data; - const { startTimeUnix, expectedEndTimeUnix } = processingData; - return { - ...state, - buyOrderState: { - processState: OrderProcessState.PROCESSING, - txHash: processingData.txHash, - progress: { - startTimeUnix, - expectedEndTimeUnix, - }, - }, - }; - case ActionTypes.SET_BUY_ORDER_STATE_FAILURE: - const failureTxHash = action.data; - if ('txHash' in state.buyOrderState) { - if (state.buyOrderState.txHash === failureTxHash) { - const { txHash, progress } = state.buyOrderState; + case ActionTypes.UPDATE_SELECTED_ASSET_AMOUNT: + return { + ...state, + selectedAssetAmount: action.data, + }; + case ActionTypes.UPDATE_LATEST_BUY_QUOTE: + const newBuyQuoteIfExists = action.data; + const shouldUpdate = + _.isUndefined(newBuyQuoteIfExists) || doesBuyQuoteMatchState(newBuyQuoteIfExists, state); + if (shouldUpdate) { return { ...state, - buyOrderState: { - processState: OrderProcessState.FAILURE, - txHash, - progress, - }, + latestBuyQuote: newBuyQuoteIfExists, + quoteRequestState: AsyncProcessState.Success, }; + } else { + return state; } - } - return state; - case ActionTypes.SET_BUY_ORDER_STATE_SUCCESS: - const successTxHash = action.data; - if ('txHash' in state.buyOrderState) { - if (state.buyOrderState.txHash === successTxHash) { - const { txHash, progress } = state.buyOrderState; - return { - ...state, - buyOrderState: { - processState: OrderProcessState.SUCCESS, - txHash, - progress, + + case ActionTypes.SET_QUOTE_REQUEST_STATE_PENDING: + return { + ...state, + latestBuyQuote: undefined, + quoteRequestState: AsyncProcessState.Pending, + }; + case ActionTypes.SET_QUOTE_REQUEST_STATE_FAILURE: + return { + ...state, + latestBuyQuote: undefined, + quoteRequestState: AsyncProcessState.Failure, + }; + case ActionTypes.SET_BUY_ORDER_STATE_NONE: + return { + ...state, + buyOrderState: { processState: OrderProcessState.None }, + }; + case ActionTypes.SET_BUY_ORDER_STATE_VALIDATING: + return { + ...state, + buyOrderState: { processState: OrderProcessState.Validating }, + }; + case ActionTypes.SET_BUY_ORDER_STATE_PROCESSING: + const processingData = action.data; + const { startTimeUnix, expectedEndTimeUnix } = processingData; + return { + ...state, + buyOrderState: { + processState: OrderProcessState.Processing, + txHash: processingData.txHash, + progress: { + startTimeUnix, + expectedEndTimeUnix, }, - }; + }, + }; + case ActionTypes.SET_BUY_ORDER_STATE_FAILURE: + const failureTxHash = action.data; + if ('txHash' in state.buyOrderState) { + if (state.buyOrderState.txHash === failureTxHash) { + const { txHash, progress } = state.buyOrderState; + return { + ...state, + buyOrderState: { + processState: OrderProcessState.Failure, + txHash, + progress, + }, + }; + } } - } - return state; - case ActionTypes.SET_ERROR_MESSAGE: - return { - ...state, - latestErrorMessage: action.data, - latestErrorDisplayStatus: DisplayStatus.Present, - }; - case ActionTypes.HIDE_ERROR: - return { - ...state, - latestErrorDisplayStatus: DisplayStatus.Hidden, - }; - case ActionTypes.CLEAR_ERROR: - return { - ...state, - latestErrorMessage: undefined, - latestErrorDisplayStatus: DisplayStatus.Hidden, - }; - case ActionTypes.UPDATE_SELECTED_ASSET: - return { - ...state, - selectedAsset: action.data, - }; - case ActionTypes.RESET_AMOUNT: - return { - ...state, - latestBuyQuote: undefined, - quoteRequestState: AsyncProcessState.NONE, - buyOrderState: { processState: OrderProcessState.NONE }, - selectedAssetAmount: undefined, - }; - case ActionTypes.SET_AVAILABLE_ASSETS: - return { - ...state, - availableAssets: action.data, - }; - default: - return state; - } + return state; + case ActionTypes.SET_BUY_ORDER_STATE_SUCCESS: + const successTxHash = action.data; + if ('txHash' in state.buyOrderState) { + if (state.buyOrderState.txHash === successTxHash) { + const { txHash, progress } = state.buyOrderState; + return { + ...state, + buyOrderState: { + processState: OrderProcessState.Success, + txHash, + progress, + }, + }; + } + } + return state; + case ActionTypes.SET_ERROR_MESSAGE: + return { + ...state, + latestErrorMessage: action.data, + latestErrorDisplayStatus: DisplayStatus.Present, + }; + case ActionTypes.HIDE_ERROR: + return { + ...state, + latestErrorDisplayStatus: DisplayStatus.Hidden, + }; + case ActionTypes.CLEAR_ERROR: + return { + ...state, + latestErrorMessage: undefined, + latestErrorDisplayStatus: DisplayStatus.Hidden, + }; + case ActionTypes.UPDATE_SELECTED_ASSET: + return { + ...state, + selectedAsset: action.data, + }; + case ActionTypes.RESET_AMOUNT: + return { + ...state, + latestBuyQuote: undefined, + quoteRequestState: AsyncProcessState.None, + buyOrderState: { processState: OrderProcessState.None }, + selectedAssetAmount: undefined, + }; + case ActionTypes.SET_AVAILABLE_ASSETS: + return { + ...state, + availableAssets: action.data, + }; + default: + return state; + } + }; + return reducer; }; const doesBuyQuoteMatchState = (buyQuote: BuyQuote, state: State): boolean => { diff --git a/packages/instant/src/redux/store.ts b/packages/instant/src/redux/store.ts index 01deb8690..20710765d 100644 --- a/packages/instant/src/redux/store.ts +++ b/packages/instant/src/redux/store.ts @@ -2,12 +2,13 @@ import * as _ from 'lodash'; import { createStore, Store as ReduxStore } from 'redux'; import { devToolsEnhancer } from 'redux-devtools-extension/developmentOnly'; -import { reducer, State } from './reducer'; +import { createReducer, State } from './reducer'; export type Store = ReduxStore<State>; export const store = { - create: (state: State): Store => { - return createStore(reducer, state, devToolsEnhancer({})); + create: (initialState: State): Store => { + const reducer = createReducer(initialState); + return createStore(reducer, initialState, devToolsEnhancer({})); }, }; diff --git a/packages/instant/src/types.ts b/packages/instant/src/types.ts index 449bc0f31..d65f70008 100644 --- a/packages/instant/src/types.ts +++ b/packages/instant/src/types.ts @@ -1,20 +1,23 @@ -import { AssetProxyId, ObjectMap } from '@0x/types'; +import { AssetBuyer, BigNumber } from '@0x/asset-buyer'; +import { AssetProxyId, ObjectMap, SignedOrder } from '@0x/types'; +import { Web3Wrapper } from '@0x/web3-wrapper'; +import { Provider } from 'ethereum-types'; // Reusable export type Maybe<T> = T | undefined; export enum AsyncProcessState { - NONE = 'None', - PENDING = 'Pending', - SUCCESS = 'Success', - FAILURE = 'Failure', + None = 'NONE', + Pending = 'PENDING', + Success = 'SUCCESS', + Failure = 'FAILURE', } export enum OrderProcessState { - NONE = 'None', - VALIDATING = 'Validating', - PROCESSING = 'Processing', - SUCCESS = 'Success', - FAILURE = 'Failure', + None = 'NONE', + Validating = 'VALIDATING', + Processing = 'PROCESSING', + Success = 'SUCCESS', + Failure = 'FAILURE', } export interface SimulatedProgress { @@ -23,10 +26,10 @@ export interface SimulatedProgress { } interface OrderStatePreTx { - processState: OrderProcessState.NONE | OrderProcessState.VALIDATING; + processState: OrderProcessState.None | OrderProcessState.Validating; } interface OrderStatePostTx { - processState: OrderProcessState.PROCESSING | OrderProcessState.SUCCESS | OrderProcessState.FAILURE; + processState: OrderProcessState.Processing | OrderProcessState.Success | OrderProcessState.Failure; txHash: string; progress: SimulatedProgress; } @@ -89,3 +92,31 @@ export interface AffiliateInfo { feeRecipient: string; feePercentage: number; } + +export interface ProviderState { + provider: Provider; + assetBuyer: AssetBuyer; + web3Wrapper: Web3Wrapper; + account: Account; +} + +export enum AccountState { + Loading = 'LOADING', + Ready = 'READY', + Locked = 'LOCKED', // TODO(bmillman): break this up into locked / privacy mode enabled + Error = 'ERROR', + None = 'NONE,', +} + +export interface AccountReady { + state: AccountState.Ready; + address: string; + ethBalanceInWei?: BigNumber; +} +export interface AccountNotReady { + state: AccountState.None | AccountState.Loading | AccountState.Locked | AccountState.Error; +} + +export type Account = AccountReady | AccountNotReady; + +export type OrderSource = string | SignedOrder[]; diff --git a/packages/instant/src/util/asset_buyer_factory.ts b/packages/instant/src/util/asset_buyer_factory.ts new file mode 100644 index 000000000..5ba46223c --- /dev/null +++ b/packages/instant/src/util/asset_buyer_factory.ts @@ -0,0 +1,17 @@ +import { AssetBuyer, AssetBuyerOpts } from '@0x/asset-buyer'; +import { Provider } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { Network, OrderSource } from '../types'; + +export const assetBuyerFactory = { + getAssetBuyer: (provider: Provider, orderSource: OrderSource, network: Network): AssetBuyer => { + const assetBuyerOptions: Partial<AssetBuyerOpts> = { + networkId: network, + }; + const assetBuyer = _.isString(orderSource) + ? AssetBuyer.getAssetBuyerForStandardRelayerAPIUrl(provider, orderSource, assetBuyerOptions) + : AssetBuyer.getAssetBuyerForProvidedOrders(provider, orderSource, assetBuyerOptions); + return assetBuyer; + }, +}; diff --git a/packages/instant/src/util/injected_provider.ts b/packages/instant/src/util/injected_provider.ts deleted file mode 100644 index 40f9e2da5..000000000 --- a/packages/instant/src/util/injected_provider.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Provider } from 'ethereum-types'; -import * as _ from 'lodash'; - -export const getInjectedProvider = (): Provider => { - const injectedProviderIfExists = (window as any).ethereum; - if (!_.isUndefined(injectedProviderIfExists)) { - // TODO: call enable here when implementing wallet connection flow - return injectedProviderIfExists; - } - const injectedWeb3IfExists = (window as any).web3; - if (!_.isUndefined(injectedWeb3IfExists.currentProvider)) { - return injectedWeb3IfExists.currentProvider; - } else { - throw new Error(`No injected web3 found`); - } -}; diff --git a/packages/instant/src/util/provider_factory.ts b/packages/instant/src/util/provider_factory.ts new file mode 100644 index 000000000..603f7674d --- /dev/null +++ b/packages/instant/src/util/provider_factory.ts @@ -0,0 +1,34 @@ +import { EmptyWalletSubprovider, RPCSubprovider, Web3ProviderEngine } from '@0x/subproviders'; +import { Provider } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { BLOCK_POLLING_INTERVAL_MS, ETHEREUM_NODE_URL_BY_NETWORK } from '../constants'; +import { Maybe, Network } from '../types'; + +export const providerFactory = { + getInjectedProviderIfExists: (): Maybe<Provider> => { + const injectedProviderIfExists = (window as any).ethereum; + if (!_.isUndefined(injectedProviderIfExists)) { + return injectedProviderIfExists; + } + const injectedWeb3IfExists = (window as any).web3; + if (!_.isUndefined(injectedWeb3IfExists) && !_.isUndefined(injectedWeb3IfExists.currentProvider)) { + return injectedWeb3IfExists.currentProvider; + } + return undefined; + }, + getFallbackNoSigningProvider: (network: Network): Provider => { + const providerEngine = new Web3ProviderEngine({ + pollingInterval: BLOCK_POLLING_INTERVAL_MS, + }); + // Intercept calls to `eth_accounts` and always return empty + providerEngine.addProvider(new EmptyWalletSubprovider()); + // Construct an RPC subprovider, all data based requests will be sent via the RPCSubprovider + // TODO(bmillman): make this more resilient to infura failures + const rpcUrl = ETHEREUM_NODE_URL_BY_NETWORK[network]; + providerEngine.addProvider(new RPCSubprovider(rpcUrl)); + // // Start the Provider Engine + providerEngine.start(); + return providerEngine; + }, +}; diff --git a/packages/instant/src/util/provider_state_factory.ts b/packages/instant/src/util/provider_state_factory.ts new file mode 100644 index 000000000..18b188d89 --- /dev/null +++ b/packages/instant/src/util/provider_state_factory.ts @@ -0,0 +1,69 @@ +import { Web3Wrapper } from '@0x/web3-wrapper'; +import { Provider } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { AccountNotReady, AccountState, Maybe, Network, OrderSource, ProviderState } from '../types'; + +import { assetBuyerFactory } from './asset_buyer_factory'; +import { providerFactory } from './provider_factory'; + +const LOADING_ACCOUNT: AccountNotReady = { + state: AccountState.Loading, +}; +const NO_ACCOUNT: AccountNotReady = { + state: AccountState.None, +}; + +export const providerStateFactory = { + getInitialProviderState: (orderSource: OrderSource, network: Network, provider?: Provider): ProviderState => { + if (!_.isUndefined(provider)) { + return providerStateFactory.getInitialProviderStateFromProvider(orderSource, network, provider); + } + const providerStateFromWindowIfExits = providerStateFactory.getInitialProviderStateFromWindowIfExists( + orderSource, + network, + ); + if (providerStateFromWindowIfExits) { + return providerStateFromWindowIfExits; + } else { + return providerStateFactory.getInitialProviderStateFallback(orderSource, network); + } + }, + getInitialProviderStateFromProvider: ( + orderSource: OrderSource, + network: Network, + provider: Provider, + ): ProviderState => { + const providerState: ProviderState = { + provider, + web3Wrapper: new Web3Wrapper(provider), + assetBuyer: assetBuyerFactory.getAssetBuyer(provider, orderSource, network), + account: LOADING_ACCOUNT, + }; + return providerState; + }, + getInitialProviderStateFromWindowIfExists: (orderSource: OrderSource, network: Network): Maybe<ProviderState> => { + const injectedProviderIfExists = providerFactory.getInjectedProviderIfExists(); + if (!_.isUndefined(injectedProviderIfExists)) { + const providerState: ProviderState = { + provider: injectedProviderIfExists, + web3Wrapper: new Web3Wrapper(injectedProviderIfExists), + assetBuyer: assetBuyerFactory.getAssetBuyer(injectedProviderIfExists, orderSource, network), + account: LOADING_ACCOUNT, + }; + return providerState; + } else { + return undefined; + } + }, + getInitialProviderStateFallback: (orderSource: OrderSource, network: Network): ProviderState => { + const provider = providerFactory.getFallbackNoSigningProvider(network); + const providerState: ProviderState = { + provider, + web3Wrapper: new Web3Wrapper(provider), + assetBuyer: assetBuyerFactory.getAssetBuyer(provider, orderSource, network), + account: NO_ACCOUNT, + }; + return providerState; + }, +}; |