diff options
author | Brandon Millman <brandon.millman@gmail.com> | 2018-11-07 12:25:30 +0800 |
---|---|---|
committer | Brandon Millman <brandon.millman@gmail.com> | 2018-11-08 01:32:49 +0800 |
commit | dfbf10c94bfbbdbca353531c5cae6707e05981f0 (patch) | |
tree | 6b1f2832742517b280d7a7594adfa6fec4ea7bef /packages | |
parent | c30dca69619bf87ed198cf375d21593213798113 (diff) | |
download | dexon-0x-contracts-dfbf10c94bfbbdbca353531c5cae6707e05981f0.tar dexon-0x-contracts-dfbf10c94bfbbdbca353531c5cae6707e05981f0.tar.gz dexon-0x-contracts-dfbf10c94bfbbdbca353531c5cae6707e05981f0.tar.bz2 dexon-0x-contracts-dfbf10c94bfbbdbca353531c5cae6707e05981f0.tar.lz dexon-0x-contracts-dfbf10c94bfbbdbca353531c5cae6707e05981f0.tar.xz dexon-0x-contracts-dfbf10c94bfbbdbca353531c5cae6707e05981f0.tar.zst dexon-0x-contracts-dfbf10c94bfbbdbca353531c5cae6707e05981f0.zip |
feat(instant): fallback to an empty wallet provider when none is injected
Diffstat (limited to 'packages')
-rw-r--r-- | packages/instant/package.json | 1 | ||||
-rw-r--r-- | packages/instant/src/components/buy_button.tsx | 8 | ||||
-rw-r--r-- | packages/instant/src/components/buy_order_state_buttons.tsx | 2 | ||||
-rw-r--r-- | packages/instant/src/components/zero_ex_instant_provider.tsx | 86 | ||||
-rw-r--r-- | packages/instant/src/constants.ts | 8 | ||||
-rw-r--r-- | packages/instant/src/containers/selected_asset_buy_order_state_buttons.ts | 46 | ||||
-rw-r--r-- | packages/instant/src/containers/selected_erc20_asset_amount_input.ts | 9 | ||||
-rw-r--r-- | packages/instant/src/redux/async_data.ts | 23 | ||||
-rw-r--r-- | packages/instant/src/redux/reducer.ts | 291 | ||||
-rw-r--r-- | packages/instant/src/redux/store.ts | 7 | ||||
-rw-r--r-- | packages/instant/src/types.ts | 26 | ||||
-rw-r--r-- | packages/instant/src/util/asset_buyer_factory.ts | 17 | ||||
-rw-r--r-- | packages/instant/src/util/injected_provider.ts | 16 | ||||
-rw-r--r-- | packages/instant/src/util/provider_factory.ts | 34 |
14 files changed, 331 insertions, 243 deletions
diff --git a/packages/instant/package.json b/packages/instant/package.json index f90990649..0f6f125fa 100644 --- a/packages/instant/package.json +++ b/packages/instant/package.json @@ -49,6 +49,7 @@ "@0x/asset-buyer": "^2.1.0", "@0x/json-schemas": "^2.0.0", "@0x/order-utils": "^2.0.0", + "@0x/subproviders": "^2.1.0", "@0x/types": "^1.2.0", "@0x/typescript-typings": "^3.0.3", "@0x/utils": "^2.0.3", 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_state_buttons.tsx b/packages/instant/src/components/buy_order_state_buttons.tsx index 2759ba341..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; diff --git a/packages/instant/src/components/zero_ex_instant_provider.tsx b/packages/instant/src/components/zero_ex_instant_provider.tsx index 0b9408329..ed0fabdf3 100644 --- a/packages/instant/src/components/zero_ex_instant_provider.tsx +++ b/packages/instant/src/components/zero_ex_instant_provider.tsx @@ -1,23 +1,22 @@ -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, ProviderState, ProviderType } from '../types'; import { assetUtils } from '../util/asset'; +import { assetBuyerFactory } from '../util/asset_buyer_factory'; import { errorFlasher } from '../util/error_flasher'; import { gasPriceEstimator } from '../util/gas_price_estimator'; -import { getInjectedProvider } from '../util/injected_provider'; +import { providerFactory } from '../util/provider_factory'; fonts.include(); @@ -25,7 +24,7 @@ export type ZeroExInstantProviderProps = ZeroExInstantProviderRequiredProps & Partial<ZeroExInstantProviderOptionalProps>; export interface ZeroExInstantProviderRequiredProps { - orderSource: string | SignedOrder[]; + orderSource: OrderSource; } export interface ZeroExInstantProviderOptionalProps { @@ -41,30 +40,43 @@ 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 = { - networkId, - }; - let assetBuyer; - if (_.isString(props.orderSource)) { - assetBuyer = AssetBuyer.getAssetBuyerForStandardRelayerAPIUrl( - provider, - props.orderSource, - 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 + let provider: Provider; + let providerType: ProviderType; + if (!_.isUndefined(props.provider)) { + provider = props.provider; + providerType = ProviderType.Props; } else { - assetBuyer = AssetBuyer.getAssetBuyerForProvidedOrders(provider, props.orderSource, assetBuyerOptions); + const injectedProviderIfExists = providerFactory.getInjectedProviderIfExists(); + if (!_.isUndefined(injectedProviderIfExists)) { + provider = injectedProviderIfExists; + providerType = ProviderType.Injected; + } else { + provider = providerFactory.getFallbackNoSigningProvider(networkId); + providerType = ProviderType.FallbackEmptyWallet; + } } + const providerState: ProviderState = { + provider, + type: providerType, + web3Wrapper: new Web3Wrapper(provider), + assetBuyer: assetBuyerFactory.getAssetBuyer(provider, props.orderSource, networkId), + }; + // 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 +86,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 +98,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 +119,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 +126,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/selected_asset_buy_order_state_buttons.ts b/packages/instant/src/containers/selected_asset_buy_order_state_buttons.ts index 1f9bb2cc2..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 || +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, - state.assetBuyer.networkId, - ); - if (etherscanUrl) { - window.open(etherscanUrl, '_blank'); - return; + 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 58257cd27..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, @@ -59,8 +59,9 @@ const mapStateToProps = (state: State, _ownProps: SelectedERC20AssetAmountInputP ? (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 1e4d7549e..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, latestErrorDisplayStatus: DisplayStatus.Hidden, quoteRequestState: AsyncProcessState.None, - affiliateInfo: undefined, }; -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 04469f8b9..0ab227310 100644 --- a/packages/instant/src/types.ts +++ b/packages/instant/src/types.ts @@ -1,4 +1,7 @@ -import { AssetProxyId, ObjectMap } from '@0x/types'; +import { AssetBuyer } 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; @@ -89,3 +92,24 @@ export interface AffiliateInfo { feeRecipient: string; feePercentage: number; } + +/** + * Our provider may be of 3 different types: + * Props: the provider was passed in a prop to the component + * Injected: the provider was injected into `window` + * FallbackEmptyWallet: the provider has no wallet but responds to data requests using infura + */ +export enum ProviderType { + Props = 'PROPS', + Injected = 'INJECTED', + FallbackEmptyWallet = 'FALLBACK_EMPTY_WALLET', +} + +export interface ProviderState { + type: ProviderType; + provider: Provider; + assetBuyer: AssetBuyer; + web3Wrapper: Web3Wrapper; +} + +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; + }, +}; |