diff options
| author | Brandon Millman <brandon@0xproject.com> | 2018-11-08 15:40:43 +0800 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2018-11-08 15:40:43 +0800 | 
| commit | f6abc007ffb249e4bbf85b8a7a77309d43e0a147 (patch) | |
| tree | 0bf9d75eed4bc60d37b7aed47be6adc2b4551a40 /packages | |
| parent | 771f8a6a6cff935631e8c6ebcdc012cdd3533de6 (diff) | |
| parent | 54b51830d075fcfc9b9e3ce0f4f4a4ef26eaf036 (diff) | |
| download | dexon-sol-tools-f6abc007ffb249e4bbf85b8a7a77309d43e0a147.tar.gz dexon-sol-tools-f6abc007ffb249e4bbf85b8a7a77309d43e0a147.tar.zst dexon-sol-tools-f6abc007ffb249e4bbf85b8a7a77309d43e0a147.zip | |
Merge pull request #1221 from 0xProject/feature/instant/fallback-provider
[instant] Ensure we always have a provider from initial state
Diffstat (limited to 'packages')
18 files changed, 422 insertions, 276 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_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; +    }, +}; | 
