diff options
29 files changed, 503 insertions, 337 deletions
diff --git a/.circleci/config.yml b/.circleci/config.yml index 7811b9b34..0ab512f58 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -31,7 +31,7 @@ jobs: - restore_cache: keys: - repo-{{ .Environment.CIRCLE_SHA1 }} - - run: cd packages/website && yarn build + - run: cd packages/website && yarn build:prod test-contracts-ganache: docker: - image: circleci/node:9 diff --git a/package.json b/package.json index c786e7f19..e598ac2d3 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ }, { "path": "packages/instant/public/main.bundle.js", - "maxSize": "500kB" + "maxSize": "1000kB" } ], "ci": { 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 57b3d108e..5b07e7416 100644 --- a/packages/instant/src/components/buy_button.tsx +++ b/packages/instant/src/components/buy_button.tsx @@ -16,7 +16,7 @@ import { Button } from './ui/button'; export interface BuyButtonProps { buyQuote?: BuyQuote; - assetBuyer?: AssetBuyer; + assetBuyer: AssetBuyer; affiliateInfo?: AffiliateInfo; onValidationPending: (buyQuote: BuyQuote) => void; onValidationFail: (buyQuote: BuyQuote, errorMessage: AssetBuyerError | ZeroExInstantError) => void; @@ -33,7 +33,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%" @@ -49,11 +49,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 45ff890b4..bdac25cf2 100644 --- a/packages/instant/src/components/buy_order_state_buttons.tsx +++ b/packages/instant/src/components/buy_order_state_buttons.tsx @@ -14,7 +14,7 @@ import { Flex } from './ui/flex'; export interface BuyOrderStateButtonProps { buyQuote?: BuyQuote; buyOrderProcessingState: OrderProcessState; - assetBuyer?: AssetBuyer; + assetBuyer: AssetBuyer; affiliateInfo?: AffiliateInfo; onViewTransaction: () => void; onValidationPending: (buyQuote: BuyQuote) => void; @@ -27,7 +27,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} fontColor={ColorOption.white} fontSize="16px"> @@ -39,11 +39,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/erc20_token_selector.tsx b/packages/instant/src/components/erc20_token_selector.tsx index 41b0b44b0..76d5c66ff 100644 --- a/packages/instant/src/components/erc20_token_selector.tsx +++ b/packages/instant/src/components/erc20_token_selector.tsx @@ -35,7 +35,7 @@ export class ERC20TokenSelector extends React.Component<ERC20TokenSelectorProps> value={this.state.searchQuery} onChange={this._handleSearchInputChange} /> - <Container overflow="scroll" height="275px" marginTop="10px"> + <Container overflow="scroll" height={{ default: '275px', sm: '75vh' }} marginTop="10px"> {_.map(tokens, token => { if (!this._isTokenQueryMatch(token)) { return null; 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/order_details.tsx b/packages/instant/src/components/order_details.tsx index f1029d70c..9abd7137e 100644 --- a/packages/instant/src/components/order_details.tsx +++ b/packages/instant/src/components/order_details.tsx @@ -26,7 +26,7 @@ export class OrderDetails extends React.Component<OrderDetailsProps> { const ethTokenFee = buyQuoteAccessor.feeEthAmount(); const totalEthAmount = buyQuoteAccessor.totalEthAmount(); return ( - <Container padding="20px" width="100%"> + <Container padding="20px" width="100%" flexGrow={1}> <Container marginBottom="10px"> <Text letterSpacing="1px" diff --git a/packages/instant/src/components/ui/container.tsx b/packages/instant/src/components/ui/container.tsx index db3f32f92..bffa1d7d4 100644 --- a/packages/instant/src/components/ui/container.tsx +++ b/packages/instant/src/components/ui/container.tsx @@ -1,17 +1,18 @@ import { darken } from 'polished'; +import { MediaChoice, stylesForMedia } from '../../style/media'; import { ColorOption, styled } from '../../style/theme'; import { cssRuleIfExists } from '../../style/util'; export interface ContainerProps { - display?: string; + display?: MediaChoice; position?: string; top?: string; right?: string; bottom?: string; left?: string; - width?: string; - height?: string; + width?: MediaChoice; + height?: MediaChoice; maxWidth?: string; margin?: string; marginTop?: string; @@ -33,6 +34,7 @@ export interface ContainerProps { cursor?: string; overflow?: string; darkenOnHover?: boolean; + flexGrow?: string | number; } export const Container = @@ -42,14 +44,12 @@ export const Container = && { all: initial; box-sizing: border-box; - ${props => cssRuleIfExists(props, 'display')} + ${props => cssRuleIfExists(props, 'flex-grow')} ${props => cssRuleIfExists(props, 'position')} ${props => cssRuleIfExists(props, 'top')} ${props => cssRuleIfExists(props, 'right')} ${props => cssRuleIfExists(props, 'bottom')} ${props => cssRuleIfExists(props, 'left')} - ${props => cssRuleIfExists(props, 'width')} - ${props => cssRuleIfExists(props, 'height')} ${props => cssRuleIfExists(props, 'max-width')} ${props => cssRuleIfExists(props, 'margin')} ${props => cssRuleIfExists(props, 'margin-top')} @@ -67,6 +67,9 @@ export const Container = ${props => cssRuleIfExists(props, 'cursor')} ${props => cssRuleIfExists(props, 'overflow')} ${props => (props.hasBoxShadow ? `box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.1)` : '')}; + ${props => props.display && stylesForMedia('display', props.display)} + ${props => (props.width ? stylesForMedia('width', props.width) : '')} + ${props => (props.height ? stylesForMedia('height', props.height) : '')} background-color: ${props => (props.backgroundColor ? props.theme[props.backgroundColor] : 'none')}; border-color: ${props => (props.borderColor ? props.theme[props.borderColor] : 'none')}; &:hover { diff --git a/packages/instant/src/components/ui/flex.tsx b/packages/instant/src/components/ui/flex.tsx index 1ec8d1f92..5b00138b8 100644 --- a/packages/instant/src/components/ui/flex.tsx +++ b/packages/instant/src/components/ui/flex.tsx @@ -1,3 +1,4 @@ +import { MediaChoice, stylesForMedia } from '../../style/media'; import { ColorOption, styled } from '../../style/theme'; import { cssRuleIfExists } from '../../style/util'; @@ -6,10 +7,11 @@ export interface FlexProps { flexWrap?: 'wrap' | 'nowrap'; justify?: 'flex-start' | 'center' | 'space-around' | 'space-between' | 'space-evenly' | 'flex-end'; align?: 'flex-start' | 'center' | 'space-around' | 'space-between' | 'space-evenly' | 'flex-end'; - width?: string; - height?: string; + width?: MediaChoice; + height?: MediaChoice; backgroundColor?: ColorOption; inline?: boolean; + flexGrow?: number | string; } export const Flex = @@ -21,11 +23,12 @@ export const Flex = display: ${props => (props.inline ? 'inline-flex' : 'flex')}; flex-direction: ${props => props.direction}; flex-wrap: ${props => props.flexWrap}; + ${props => cssRuleIfExists(props, 'flexGrow')} justify-content: ${props => props.justify}; align-items: ${props => props.align}; - ${props => cssRuleIfExists(props, 'width')} - ${props => cssRuleIfExists(props, 'height')} background-color: ${props => (props.backgroundColor ? props.theme[props.backgroundColor] : 'none')}; + ${props => (props.width ? stylesForMedia('width', props.width) : '')} + ${props => (props.height ? stylesForMedia('height', props.height) : '')} } `; diff --git a/packages/instant/src/components/ui/overlay.tsx b/packages/instant/src/components/ui/overlay.tsx index deb16a0e8..8c9572615 100644 --- a/packages/instant/src/components/ui/overlay.tsx +++ b/packages/instant/src/components/ui/overlay.tsx @@ -15,10 +15,12 @@ export interface OverlayProps { const PlainOverlay: React.StatelessComponent<OverlayProps> = ({ children, className, onClose }) => ( <Flex height="100vh" className={className}> - <Container position="absolute" top="0px" right="0px"> + <Container position="absolute" top="0px" right="0px" display={{ default: 'initial', sm: 'none' }}> <Icon height={18} width={18} color={ColorOption.white} icon="closeX" onClick={onClose} padding="2em 2em" /> </Container> - <div>{children}</div> + <Container width={{ default: 'auto', sm: '100%' }} height={{ default: 'auto', sm: '100%' }}> + {children} + </Container> </Flex> ); export const Overlay = styled(PlainOverlay)` diff --git a/packages/instant/src/components/zero_ex_instant_container.tsx b/packages/instant/src/components/zero_ex_instant_container.tsx index 3f270f2f4..f5f8adefe 100644 --- a/packages/instant/src/components/zero_ex_instant_container.tsx +++ b/packages/instant/src/components/zero_ex_instant_container.tsx @@ -30,7 +30,11 @@ export class ZeroExInstantContainer extends React.Component<ZeroExInstantContain return ( <React.Fragment> <CSSReset /> - <Container width="350px" position="relative"> + <Container + width={{ default: '350px', sm: '100%' }} + height={{ default: 'auto', sm: '100%' }} + position="relative" + > <Container zIndex={zIndex.errorPopup} position="relative"> <LatestError /> </Container> @@ -41,8 +45,9 @@ export class ZeroExInstantContainer extends React.Component<ZeroExInstantContain borderRadius="3px" hasBoxShadow={true} overflow="hidden" + height="100%" > - <Flex direction="column" justify="flex-start"> + <Flex direction="column" justify="flex-start" height="100%"> <SelectedAssetInstantHeading onSelectAssetClick={this._handleSymbolClick} /> <SelectedAssetBuyOrderProgress /> <LatestBuyQuoteOrderDetails /> 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 12cbe3a70..34548f26f 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'; @@ -14,3 +17,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/style/media.ts b/packages/instant/src/style/media.ts new file mode 100644 index 000000000..beabbac46 --- /dev/null +++ b/packages/instant/src/style/media.ts @@ -0,0 +1,43 @@ +import { InterpolationValue } from 'styled-components'; + +import { css } from './theme'; + +export enum ScreenWidths { + Sm = 40, + Md = 52, + Lg = 64, +} + +const generateMediaWrapper = (screenWidth: ScreenWidths) => (...args: any[]) => css` + @media (max-width: ${screenWidth}em) { + ${css.apply(css, args)}; + } +`; + +const media = { + small: generateMediaWrapper(ScreenWidths.Sm), + medium: generateMediaWrapper(ScreenWidths.Md), + large: generateMediaWrapper(ScreenWidths.Lg), +}; + +export interface ScreenSpecifications { + default: string; + sm?: string; + md?: string; + lg?: string; +} +export type MediaChoice = string | ScreenSpecifications; +export const stylesForMedia = (cssPropertyName: string, choice: MediaChoice): InterpolationValue[] => { + if (typeof choice === 'string') { + return css` + ${cssPropertyName}: ${choice}; + `; + } + + return css` + ${cssPropertyName}: ${choice.default}; + ${choice.lg && media.large`${cssPropertyName}: ${choice.lg}`} + ${choice.md && media.medium`${cssPropertyName}: ${choice.md}`} + ${choice.sm && media.small`${cssPropertyName}: ${choice.sm}`} + `; +}; 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; + }, +}; diff --git a/packages/website/package.json b/packages/website/package.json index efb97d309..dd14d4b17 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -7,14 +7,15 @@ "private": true, "description": "Website and 0x portal dapp", "scripts": { - "build": "node --max_old_space_size=8192 ../../node_modules/.bin/webpack --mode production", + "build": "yarn build:dev", + "build:prod": "node --max_old_space_size=8192 ../../node_modules/.bin/webpack --mode production", "build:dev": "../../node_modules/.bin/webpack --mode development", "clean": "shx rm -f public/bundle*", "lint": "tslint --format stylish --project . 'ts/**/*.ts' 'ts/**/*.tsx'", "dev": "webpack-dev-server --mode development --content-base public --https", - "deploy_dogfood": "npm run build; aws s3 sync ./public/. s3://dogfood.0xproject.com --profile 0xproject --region us-east-1 --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers", - "deploy_staging": "npm run build; aws s3 sync ./public/. s3://staging-0xproject --profile 0xproject --region us-east-1 --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers", - "deploy_live": "DEPLOY_ROLLBAR_SOURCEMAPS=true npm run build; aws s3 sync ./public/. s3://0xproject.com --profile 0xproject --region us-east-1 --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --exclude *.map.js" + "deploy_dogfood": "npm run build:prod; aws s3 sync ./public/. s3://dogfood.0xproject.com --profile 0xproject --region us-east-1 --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers", + "deploy_staging": "npm run build:prod; aws s3 sync ./public/. s3://staging-0xproject --profile 0xproject --region us-east-1 --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers", + "deploy_live": "DEPLOY_ROLLBAR_SOURCEMAPS=true npm run build:prod; aws s3 sync ./public/. s3://0xproject.com --profile 0xproject --region us-east-1 --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --exclude *.map.js" }, "author": "Fabio Berger", "license": "Apache-2.0", @@ -1900,10 +1900,6 @@ aes-js@^0.2.3: version "0.2.4" resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-0.2.4.tgz#94b881ab717286d015fa219e08fb66709dda5a3d" -aes-js@^3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/aes-js/-/aes-js-3.1.1.tgz#89fd1f94ae51b4c72d62466adc1a7323ff52f072" - agent-base@4, agent-base@^4.1.0, agent-base@~4.2.0: version "4.2.1" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" @@ -3341,7 +3337,7 @@ bs-logger@0.x: dependencies: fast-json-stable-stringify "^2.0.0" -bs58@=4.0.1, bs58@^4.0.0: +bs58@=4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a" dependencies: @@ -3364,14 +3360,6 @@ bs58check@^1.0.8: bs58 "^3.1.0" create-hash "^1.1.0" -bs58check@^2.1.2: - version "2.1.2" - resolved "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz#53b018291228d82a5aa08e7d796fdafda54aebfc" - dependencies: - bs58 "^4.0.0" - create-hash "^1.1.0" - safe-buffer "^5.1.2" - bser@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719" @@ -5959,19 +5947,6 @@ ethereumjs-wallet@0.6.0: utf8 "^2.1.1" uuid "^2.0.1" -ethereumjs-wallet@~0.6.0: - version "0.6.2" - resolved "https://registry.npmjs.org/ethereumjs-wallet/-/ethereumjs-wallet-0.6.2.tgz#67244b6af3e8113b53d709124b25477b64aeccda" - dependencies: - aes-js "^3.1.1" - bs58check "^2.1.2" - ethereumjs-util "^5.2.0" - hdkey "^1.0.0" - safe-buffer "^5.1.2" - scrypt.js "^0.2.0" - utf8 "^3.0.0" - uuid "^3.3.2" - ethers@~4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/ethers/-/ethers-4.0.4.tgz#d3f85e8b27f4b59537e06526439b0fb15b44dc65" @@ -6776,7 +6751,7 @@ ganache-core@0xProject/ganache-core#monorepo-dep: ethereumjs-tx "0xProject/ethereumjs-tx#fake-tx-include-signature-by-default" ethereumjs-util "^5.2.0" ethereumjs-vm "2.3.5" - ethereumjs-wallet "~0.6.0" + ethereumjs-wallet "0.6.0" fake-merkle-patricia-tree "~1.0.1" heap "~0.2.6" js-scrypt "^0.2.0" @@ -7501,14 +7476,6 @@ hdkey@^0.7.0, hdkey@^0.7.1: coinstring "^2.0.0" secp256k1 "^3.0.1" -hdkey@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/hdkey/-/hdkey-1.1.0.tgz#e74e7b01d2c47f797fa65d1d839adb7a44639f29" - dependencies: - coinstring "^2.0.0" - safe-buffer "^5.1.1" - secp256k1 "^3.0.1" - he@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" @@ -15595,10 +15562,6 @@ utf8@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/utf8/-/utf8-2.1.2.tgz#1fa0d9270e9be850d9b05027f63519bf46457d96" -utf8@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz#f052eed1364d696e769ef058b183df88c87f69d1" - util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" |