diff options
author | fragosti <francesco.agosti93@gmail.com> | 2018-11-02 07:13:43 +0800 |
---|---|---|
committer | fragosti <francesco.agosti93@gmail.com> | 2018-11-02 07:13:43 +0800 |
commit | 695d1453ac941fd7ef79ff1c4a255630eb76c764 (patch) | |
tree | 8e18b543df236ce7ef00d33770ed71f91e6b7d67 | |
parent | ce19ec207b07696a675bdadf71ea39d933ea2715 (diff) | |
parent | 7c30fd4b2da83c9522f9137f4d18e6c308f2b66f (diff) | |
download | dexon-sol-tools-695d1453ac941fd7ef79ff1c4a255630eb76c764.tar dexon-sol-tools-695d1453ac941fd7ef79ff1c4a255630eb76c764.tar.gz dexon-sol-tools-695d1453ac941fd7ef79ff1c4a255630eb76c764.tar.bz2 dexon-sol-tools-695d1453ac941fd7ef79ff1c4a255630eb76c764.tar.lz dexon-sol-tools-695d1453ac941fd7ef79ff1c4a255630eb76c764.tar.xz dexon-sol-tools-695d1453ac941fd7ef79ff1c4a255630eb76c764.tar.zst dexon-sol-tools-695d1453ac941fd7ef79ff1c4a255630eb76c764.zip |
Merge branch 'development' of https://github.com/0xProject/0x-monorepo into feature/instant/maker-asset-datas-interface
20 files changed, 404 insertions, 259 deletions
diff --git a/packages/asset-buyer/CHANGELOG.json b/packages/asset-buyer/CHANGELOG.json index c572ba4cf..6ba2a0fd9 100644 --- a/packages/asset-buyer/CHANGELOG.json +++ b/packages/asset-buyer/CHANGELOG.json @@ -10,6 +10,10 @@ "note": "the `OrderProvider` now requires a new method `getAvailableMakerAssetDatasAsync` and the `StandardRelayerAPIOrderProvider` requires the network id at init.", "pr": 1203 + }, + { + "note": "No longer require that provided orders all have the same maker and taker asset data", + "pr": 1197 } ] }, diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts index 2bc80ac12..49743404f 100644 --- a/packages/asset-buyer/src/asset_buyer.ts +++ b/packages/asset-buyer/src/asset_buyer.ts @@ -56,7 +56,6 @@ export class AssetBuyer { ): AssetBuyer { assert.isWeb3Provider('provider', provider); assert.doesConformToSchema('orders', orders, schemas.signedOrdersSchema); - assert.areValidProvidedOrders('orders', orders); assert.assert(orders.length !== 0, `Expected orders to contain at least one order`); const orderProvider = new BasicOrderProvider(orders); const assetBuyer = new AssetBuyer(provider, orderProvider, options); diff --git a/packages/asset-buyer/src/constants.ts b/packages/asset-buyer/src/constants.ts index b2f025bdb..cc415102c 100644 --- a/packages/asset-buyer/src/constants.ts +++ b/packages/asset-buyer/src/constants.ts @@ -36,6 +36,5 @@ export const constants = { DEFAULT_ASSET_BUYER_OPTS, DEFAULT_BUY_QUOTE_EXECUTION_OPTS, DEFAULT_BUY_QUOTE_REQUEST_OPTS, - MAX_PER_PAGE: 1000, EMPTY_ORDERS_AND_FILLABLE_AMOUNTS, }; diff --git a/packages/asset-buyer/src/order_providers/standard_relayer_api_order_provider.ts b/packages/asset-buyer/src/order_providers/standard_relayer_api_order_provider.ts index c6adcfc4a..be1fc55d6 100644 --- a/packages/asset-buyer/src/order_providers/standard_relayer_api_order_provider.ts +++ b/packages/asset-buyer/src/order_providers/standard_relayer_api_order_provider.ts @@ -2,7 +2,6 @@ import { HttpClient } from '@0x/connect'; import { APIOrder, AssetPairsResponse, OrderbookResponse } from '@0x/types'; import * as _ from 'lodash'; -import { constants } from '../constants'; import { AssetBuyerError, OrderProvider, @@ -87,7 +86,9 @@ export class StandardRelayerAPIOrderProvider implements OrderProvider { * @return An array of asset data strings that can be purchased using takerAssetData. */ public async getAvailableMakerAssetDatasAsync(takerAssetData: string): Promise<string[]> { - const requestOpts = { networkId: this.networkId, perPage: constants.MAX_PER_PAGE }; + // Return a maximum of 1000 asset datas + const maxPerPage = 1000; + const requestOpts = { networkId: this.networkId, perPage: maxPerPage }; const assetPairsRequest = { assetDataA: takerAssetData }; const fullRequest = { ...requestOpts, diff --git a/packages/asset-buyer/src/utils/assert.ts b/packages/asset-buyer/src/utils/assert.ts index e5bfd2d81..2466f53a4 100644 --- a/packages/asset-buyer/src/utils/assert.ts +++ b/packages/asset-buyer/src/utils/assert.ts @@ -1,6 +1,5 @@ import { assert as sharedAssert } from '@0x/assert'; import { schemas } from '@0x/json-schemas'; -import { SignedOrder } from '@0x/types'; import * as _ from 'lodash'; import { BuyQuote, BuyQuoteInfo, OrderProvider, OrderProviderRequest } from '../types'; @@ -30,21 +29,6 @@ export const assert = { sharedAssert.isHexString(`${variableName}.makerAssetData`, orderFetcherRequest.makerAssetData); sharedAssert.isHexString(`${variableName}.takerAssetData`, orderFetcherRequest.takerAssetData); }, - areValidProvidedOrders(variableName: string, orders: SignedOrder[]): void { - if (orders.length === 0) { - return; - } - const makerAssetData = orders[0].makerAssetData; - const takerAssetData = orders[0].takerAssetData; - const filteredOrders = _.filter( - orders, - order => order.makerAssetData === makerAssetData && order.takerAssetData === takerAssetData, - ); - sharedAssert.assert( - orders.length === filteredOrders.length, - `Expected all orders in ${variableName} to have the same makerAssetData and takerAssetData.`, - ); - }, isValidPercentage(variableName: string, percentage: number): void { assert.isNumber(variableName, percentage); assert.assert( diff --git a/packages/instant/public/index.html b/packages/instant/public/index.html index 4c35e6f3b..e1f8432be 100644 --- a/packages/instant/public/index.html +++ b/packages/instant/public/index.html @@ -34,28 +34,47 @@ EXPONENTIAL_AT: 1000, DECIMAL_PLACES: 78, }); - const providedOrder = { - senderAddress: '0x0000000000000000000000000000000000000000', - makerAddress: '0x14e2f1f157e7dd4057d02817436d628a37120fd1', - takerAddress: '0x0000000000000000000000000000000000000000', - makerFee: new BigNumber('0'), - takerFee: new BigNumber('0'), - makerAssetAmount: new BigNumber('100000000000000000000'), - takerAssetAmount: new BigNumber('10000000000000000'), - makerAssetData: '0xf47261b00000000000000000000000002002d3812f58e35f0ea1ffbf80a75a38c32175fa', - takerAssetData: '0xf47261b0000000000000000000000000d0a1e359811322d97991e03f863a0c30c2cf029c', - expirationTimeSeconds: new BigNumber('1591858800'), - feeRecipientAddress: '0x0000000000000000000000000000000000000000', - salt: new BigNumber( - '54983920541892966634674340965984367456810207583416050222519063020710969340046', - ), - signature: - '0x1b949656218421c845995457303569a656764afa2b979d41dcefff0009d57ce15001490268bc7caa4269894fd83b741465fc5a7a53eda6ece17eb91fb32655d83703', - exchangeAddress: '0x35dd2932454449b14cee11a94d3674a936d5d7b2', - }; + const providedOrders = [ + // Order selling REP + { + senderAddress: '0x0000000000000000000000000000000000000000', + makerAddress: '0x34a745008a643eebc58920eaa29fb1165b4a288e', + takerAddress: '0x0000000000000000000000000000000000000000', + makerFee: new BigNumber('0'), + takerFee: new BigNumber('0'), + makerAssetAmount: new BigNumber('400000000000000000000'), + takerAssetAmount: new BigNumber('40000000000000000000'), + makerAssetData: '0xf47261b00000000000000000000000008cb3971b8eb709c14616bd556ff6683019e90d9c', + takerAssetData: '0xf47261b0000000000000000000000000d0a1e359811322d97991e03f863a0c30c2cf029c', + expirationTimeSeconds: new BigNumber('1543046400'), + feeRecipientAddress: '0x0000000000000000000000000000000000000000', + salt: new BigNumber('47929252863126413473766089649682650973189811771354566206928245255479607883031'), + signature: '0x1c0bf8ba709ceb5b32e6b0b5a8bb7f07e9d19aba88d8530715f8a298d12188e3862fcc0a30ddfad4062b30459f2859323c064052f12cc687466c457934b9419a1b03', + exchangeAddress: '0x35dd2932454449b14cee11a94d3674a936d5d7b2' + }, + // Order selling ZRX + { + senderAddress: '0x0000000000000000000000000000000000000000', + makerAddress: '0x34a745008a643eebc58920eaa29fb1165b4a288e', + takerAddress: '0x0000000000000000000000000000000000000000', + makerFee: new BigNumber('0'), + takerFee: new BigNumber('0'), + makerAssetAmount: new BigNumber('300000000000000000000'), + takerAssetAmount: new BigNumber('31000000000000000000'), + makerAssetData: '0xf47261b00000000000000000000000002002d3812f58e35f0ea1ffbf80a75a38c32175fa', + takerAssetData: '0xf47261b0000000000000000000000000d0a1e359811322d97991e03f863a0c30c2cf029c', + expirationTimeSeconds: new BigNumber('2524636800'), + feeRecipientAddress: '0x0000000000000000000000000000000000000000', + salt: new BigNumber('64592004666704945574675477805199411288137454783320798602050822322450089238268'), + signature: '0x1c13cacddca8d7d8248e91f412377e68f8f1f9891a59a6c1b2eea9f7b33558c30c4fb86a448e08ab7def40a28fb3a3062dcb33bb3c45302447fce5c4288b7c7f5b03', + exchangeAddress: '0x35dd2932454449b14cee11a94d3674a936d5d7b2' + } + ]; const queryParams = new Uri(window.location.search); const renderOptionsDefaults = { orderSource: 'https://api.radarrelay.com/0x/v2/', + assetData: '0xf47261b0000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498', + onClose: () => { console.log('0x Instant Closed') } } const orderSourceOverride = queryParams.getQueryParamValue('orderSource'); const availableAssetDatasString = queryParams.getQueryParamValue('availableAssetDatas'); diff --git a/packages/instant/src/components/ui/icon.tsx b/packages/instant/src/components/ui/icon.tsx index 61b382760..574cb26b7 100644 --- a/packages/instant/src/components/ui/icon.tsx +++ b/packages/instant/src/components/ui/icon.tsx @@ -1,5 +1,8 @@ +import * as _ from 'lodash'; import * as React from 'react'; +import { styled } from '../../style/theme'; + type svgRule = 'evenodd' | 'nonzero' | 'inherit'; interface IconInfo { viewBox: string; @@ -13,11 +16,19 @@ interface IconInfo { strokeLinejoin?: 'miter' | 'round' | 'bevel' | 'inherit'; } interface IconInfoMapping { + closeX: IconInfo; failed: IconInfo; success: IconInfo; chevron: IconInfo; } const ICONS: IconInfoMapping = { + closeX: { + viewBox: '0 0 11 11', + fillRule: 'evenodd', + clipRule: 'evenodd', + path: + 'M10.45 10.449C10.7539 10.1453 10.7539 9.65282 10.45 9.34909L6.60068 5.49999L10.45 1.65093C10.7538 1.3472 10.7538 0.854765 10.45 0.551038C10.1462 0.24731 9.65378 0.24731 9.34995 0.551038L5.50058 4.40006L1.65024 0.549939C1.34641 0.246212 0.853973 0.246212 0.550262 0.549939C0.246429 0.853667 0.246429 1.34611 0.550262 1.64983L4.40073 5.49995L0.55014 9.35019C0.246307 9.65392 0.246307 10.1464 0.55014 10.4501C0.853851 10.7538 1.34628 10.7538 1.65012 10.4501L5.5007 6.59987L9.35007 10.449C9.6539 10.7527 10.1463 10.7527 10.45 10.449Z', + }, failed: { viewBox: '0 0 34 34', fillRule: 'evenodd', @@ -44,33 +55,56 @@ const ICONS: IconInfoMapping = { }; export interface IconProps { + className?: string; width: number; height?: number; color?: string; icon: keyof IconInfoMapping; + onClick?: (event: React.MouseEvent<HTMLElement>) => void; + padding?: string; } -export const Icon: React.SFC<IconProps> = props => { +const PlainIcon: React.SFC<IconProps> = props => { const iconInfo = ICONS[props.icon]; - return ( - <svg - width={props.width} - height={props.height} - viewBox={iconInfo.viewBox} - fill="none" - xmlns="http://www.w3.org/2000/svg" - > - <path - d={iconInfo.path} - fill={props.color} - fillRule={iconInfo.fillRule || 'nonzero'} - clipRule={iconInfo.clipRule || 'nonzero'} - stroke={iconInfo.stroke} - strokeOpacity={iconInfo.strokeOpacity} - strokeWidth={iconInfo.strokeWidth} - strokeLinecap={iconInfo.strokeLinecap} - strokeLinejoin={iconInfo.strokeLinejoin} - /> - </svg> + <div onClick={props.onClick} className={props.className}> + <svg + width={props.width} + height={props.height} + viewBox={iconInfo.viewBox} + fill="none" + xmlns="http://www.w3.org/2000/svg" + > + <path + d={iconInfo.path} + fill={props.color} + fillRule={iconInfo.fillRule || 'nonzero'} + clipRule={iconInfo.clipRule || 'nonzero'} + stroke={iconInfo.stroke} + strokeOpacity={iconInfo.strokeOpacity} + strokeWidth={iconInfo.strokeWidth} + strokeLinecap={iconInfo.strokeLinecap} + strokeLinejoin={iconInfo.strokeLinejoin} + /> + </svg> + </div> ); }; + +export const Icon = styled(PlainIcon)` + cursor: ${props => (!_.isUndefined(props.onClick) ? 'pointer' : 'default')}; + transition: opacity 0.5s ease; + padding: ${props => props.padding}; + opacity: ${props => (!_.isUndefined(props.onClick) ? 0.7 : 1)}; + &:hover { + opacity: 1; + } + &:active { + opacity: 1; + } +`; + +Icon.defaultProps = { + padding: '0em 0em', +}; + +Icon.displayName = 'Icon'; diff --git a/packages/instant/src/components/ui/index.ts b/packages/instant/src/components/ui/index.ts index 28f76c262..0efabdb85 100644 --- a/packages/instant/src/components/ui/index.ts +++ b/packages/instant/src/components/ui/index.ts @@ -1,7 +1,8 @@ -export { Text, Title } from './text'; +export { Text, TextProps, Title } from './text'; export { Button, ButtonProps } from './button'; export { Flex, FlexProps } from './flex'; export { Container, ContainerProps } from './container'; export { Input, InputProps } from './input'; export { Icon, IconProps } from './icon'; export { Spinner, SpinnerProps } from './spinner'; +export { Overlay, OverlayProps } from './overlay'; diff --git a/packages/instant/src/components/ui/overlay.tsx b/packages/instant/src/components/ui/overlay.tsx new file mode 100644 index 000000000..c5258b031 --- /dev/null +++ b/packages/instant/src/components/ui/overlay.tsx @@ -0,0 +1,38 @@ +import * as _ from 'lodash'; +import * as React from 'react'; + +import { overlayBlack, styled } from '../../style/theme'; + +import { Container } from './container'; +import { Flex } from './flex'; +import { Icon } from './icon'; + +export interface OverlayProps { + className?: string; + onClose?: () => void; + zIndex?: number; +} + +const PlainOverlay: React.StatelessComponent<OverlayProps> = ({ children, className, onClose }) => ( + <Flex height="100vh" className={className}> + <Container position="absolute" top="0px" right="0px"> + <Icon height={18} width={18} color="white" icon="closeX" onClick={onClose} padding="2em 2em" /> + </Container> + <div>{children}</div> + </Flex> +); +export const Overlay = styled(PlainOverlay)` + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: ${props => props.zIndex} + background-color: ${overlayBlack}; +`; + +Overlay.defaultProps = { + zIndex: 100, +}; + +Overlay.displayName = 'Overlay'; diff --git a/packages/instant/src/components/zero_ex_instant.tsx b/packages/instant/src/components/zero_ex_instant.tsx index f3b212058..907c42e7a 100644 --- a/packages/instant/src/components/zero_ex_instant.tsx +++ b/packages/instant/src/components/zero_ex_instant.tsx @@ -1,127 +1,14 @@ -import { AssetBuyer } from '@0x/asset-buyer'; -import { ObjectMap, SignedOrder } from '@0x/types'; -import * as _ from 'lodash'; import * as React from 'react'; -import { Provider } from 'react-redux'; - -import { SelectedAssetThemeProvider } from '../containers/selected_asset_theme_provider'; -import { asyncData } from '../redux/async_data'; -import { INITIAL_STATE, State } from '../redux/reducer'; -import { store, Store } from '../redux/store'; -import { fonts } from '../style/fonts'; -import { AssetMetaData, Network } from '../types'; -import { assetUtils } from '../util/asset'; -import { BigNumberInput } from '../util/big_number_input'; -import { errorFlasher } from '../util/error_flasher'; -import { gasPriceEstimator } from '../util/gas_price_estimator'; -import { getProvider } from '../util/provider'; -import { web3Wrapper } from '../util/web3_wrapper'; import { ZeroExInstantContainer } from './zero_ex_instant_container'; +import { ZeroExInstantProvider, ZeroExInstantProviderProps } from './zero_ex_instant_provider'; -fonts.include(); - -export type ZeroExInstantProps = ZeroExInstantRequiredProps & Partial<ZeroExInstantOptionalProps>; - -export interface ZeroExInstantRequiredProps { - orderSource: string | SignedOrder[]; -} - -export interface ZeroExInstantOptionalProps { - availableAssetDatas: string[]; - defaultAssetBuyAmount: number; - defaultSelectedAssetData: string; - additionalAssetMetaDataMap: ObjectMap<AssetMetaData>; - networkId: Network; -} - -export class ZeroExInstant extends React.Component<ZeroExInstantProps> { - private readonly _store: Store; - // TODO(fragosti): Write tests for this beast once we inject a provider. - private static _mergeInitialStateWithProps(props: ZeroExInstantProps, state: State = INITIAL_STATE): State { - const networkId = props.networkId || state.network; - // TODO: Provider needs to not be hard-coded to injected web3. - const provider = getProvider(); - const assetBuyerOptions = { - networkId, - }; - let assetBuyer; - if (_.isString(props.orderSource)) { - assetBuyer = AssetBuyer.getAssetBuyerForStandardRelayerAPIUrl( - provider, - props.orderSource, - assetBuyerOptions, - ); - } else { - assetBuyer = AssetBuyer.getAssetBuyerForProvidedOrders(provider, props.orderSource, assetBuyerOptions); - } - const completeAssetMetaDataMap = { - ...props.additionalAssetMetaDataMap, - ...state.assetMetaDataMap, - }; - const storeStateFromProps: State = { - ...state, - assetBuyer, - network: networkId, - selectedAsset: _.isUndefined(props.defaultSelectedAssetData) - ? undefined - : assetUtils.createAssetFromAssetDataOrThrow( - props.defaultSelectedAssetData, - completeAssetMetaDataMap, - networkId, - ), - selectedAssetAmount: _.isUndefined(props.defaultAssetBuyAmount) - ? state.selectedAssetAmount - : new BigNumberInput(props.defaultAssetBuyAmount), - availableAssets: _.isUndefined(props.availableAssetDatas) - ? undefined - : assetUtils.createAssetsFromAssetDatas(props.availableAssetDatas, completeAssetMetaDataMap, networkId), - assetMetaDataMap: completeAssetMetaDataMap, - }; - return storeStateFromProps; - } - constructor(props: ZeroExInstantProps) { - super(props); - const initialAppState = ZeroExInstant._mergeInitialStateWithProps(this.props, INITIAL_STATE); - this._store = store.create(initialAppState); - } - - public componentDidMount(): void { - const state = this._store.getState(); - // tslint:disable-next-line:no-floating-promises - asyncData.fetchEthPriceAndDispatchToStore(this._store); - // fetch available assets if none are specified - if (_.isUndefined(state.availableAssets)) { - // tslint:disable-next-line:no-floating-promises - asyncData.fetchAvailableAssetDatasAndDispatchToStore(this._store); - } - - // warm up the gas price estimator cache just in case we can't - // grab the gas price estimate when submitting the transaction - // tslint:disable-next-line:no-floating-promises - gasPriceEstimator.getFastAmountInWeiAsync(); - - // tslint:disable-next-line:no-floating-promises - this._flashErrorIfWrongNetwork(); - } - - public render(): React.ReactNode { - return ( - <Provider store={this._store}> - <SelectedAssetThemeProvider> - <ZeroExInstantContainer /> - </SelectedAssetThemeProvider> - </Provider> - ); - } +export type ZeroExInstantProps = ZeroExInstantProviderProps; - private readonly _flashErrorIfWrongNetwork = async (): Promise<void> => { - const msToShowError = 30000; // 30 seconds - const network = this._store.getState().network; - 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); - } - }; -} +export const ZeroExInstant: React.StatelessComponent<ZeroExInstantProps> = props => { + return ( + <ZeroExInstantProvider {...props}> + <ZeroExInstantContainer /> + </ZeroExInstantProvider> + ); +}; diff --git a/packages/instant/src/components/zero_ex_instant_overlay.tsx b/packages/instant/src/components/zero_ex_instant_overlay.tsx new file mode 100644 index 000000000..8f872f896 --- /dev/null +++ b/packages/instant/src/components/zero_ex_instant_overlay.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; + +import { Overlay } from './ui'; +import { ZeroExInstantContainer } from './zero_ex_instant_container'; +import { ZeroExInstantProvider, ZeroExInstantProviderProps } from './zero_ex_instant_provider'; + +export interface ZeroExInstantOverlayProps extends ZeroExInstantProviderProps { + onClose?: () => void; + zIndex?: number; +} + +export const ZeroExInstantOverlay: React.StatelessComponent<ZeroExInstantOverlayProps> = props => { + const { onClose, zIndex, ...rest } = props; + return ( + <ZeroExInstantProvider {...rest}> + <Overlay onClose={onClose} zIndex={zIndex}> + <ZeroExInstantContainer /> + </Overlay> + </ZeroExInstantProvider> + ); +}; diff --git a/packages/instant/src/components/zero_ex_instant_provider.tsx b/packages/instant/src/components/zero_ex_instant_provider.tsx new file mode 100644 index 000000000..8552a7fb5 --- /dev/null +++ b/packages/instant/src/components/zero_ex_instant_provider.tsx @@ -0,0 +1,126 @@ +import { AssetBuyer } from '@0x/asset-buyer'; +import { ObjectMap, SignedOrder } from '@0x/types'; +import * as _ from 'lodash'; +import * as React from 'react'; +import { Provider } from 'react-redux'; + +import { SelectedAssetThemeProvider } from '../containers/selected_asset_theme_provider'; +import { asyncData } from '../redux/async_data'; +import { INITIAL_STATE, State } from '../redux/reducer'; +import { store, Store } from '../redux/store'; +import { fonts } from '../style/fonts'; +import { AssetMetaData, Network } from '../types'; +import { assetUtils } from '../util/asset'; +import { BigNumberInput } from '../util/big_number_input'; +import { errorFlasher } from '../util/error_flasher'; +import { gasPriceEstimator } from '../util/gas_price_estimator'; +import { getProvider } from '../util/provider'; +import { web3Wrapper } from '../util/web3_wrapper'; + +import { ZeroExInstantContainer } from './zero_ex_instant_container'; + +fonts.include(); + +export type ZeroExInstantProviderProps = ZeroExInstantProviderRequiredProps & + Partial<ZeroExInstantProviderOptionalProps>; + +export interface ZeroExInstantProviderRequiredProps { + orderSource: string | SignedOrder[]; +} + +export interface ZeroExInstantProviderOptionalProps { + availableAssetDatas: string[]; + defaultAssetBuyAmount: number; + defaultSelectedAssetData: string; + additionalAssetMetaDataMap: ObjectMap<AssetMetaData>; + networkId: Network; +} + +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: Provider needs to not be hard-coded to injected web3. + const provider = getProvider(); + const assetBuyerOptions = { + networkId, + }; + let assetBuyer; + if (_.isString(props.orderSource)) { + assetBuyer = AssetBuyer.getAssetBuyerForStandardRelayerAPIUrl( + provider, + props.orderSource, + assetBuyerOptions, + ); + } else { + assetBuyer = AssetBuyer.getAssetBuyerForProvidedOrders(provider, props.orderSource, assetBuyerOptions); + } + const completeAssetMetaDataMap = { + ...props.additionalAssetMetaDataMap, + ...state.assetMetaDataMap, + }; + const storeStateFromProps: State = { + ...state, + assetBuyer, + network: networkId, + selectedAsset: _.isUndefined(props.defaultSelectedAssetData) + ? undefined + : assetUtils.createAssetFromAssetDataOrThrow( + props.defaultSelectedAssetData, + completeAssetMetaDataMap, + networkId, + ), + selectedAssetAmount: _.isUndefined(props.defaultAssetBuyAmount) + ? state.selectedAssetAmount + : new BigNumberInput(props.defaultAssetBuyAmount), + availableAssets: _.isUndefined(props.availableAssetDatas) + ? undefined + : assetUtils.createAssetsFromAssetDatas(props.availableAssetDatas, completeAssetMetaDataMap, networkId), + assetMetaDataMap: completeAssetMetaDataMap, + }; + return storeStateFromProps; + } + constructor(props: ZeroExInstantProviderProps) { + super(props); + const initialAppState = ZeroExInstantProvider._mergeInitialStateWithProps(this.props, INITIAL_STATE); + this._store = store.create(initialAppState); + } + + public componentDidMount(): void { + const state = this._store.getState(); + // tslint:disable-next-line:no-floating-promises + asyncData.fetchEthPriceAndDispatchToStore(this._store); + // fetch available assets if none are specified + if (_.isUndefined(state.availableAssets)) { + // tslint:disable-next-line:no-floating-promises + asyncData.fetchAvailableAssetDatasAndDispatchToStore(this._store); + } + + // warm up the gas price estimator cache just in case we can't + // grab the gas price estimate when submitting the transaction + // tslint:disable-next-line:no-floating-promises + gasPriceEstimator.getFastAmountInWeiAsync(); + + // tslint:disable-next-line:no-floating-promises + this._flashErrorIfWrongNetwork(); + } + + public render(): React.ReactNode { + return ( + <Provider store={this._store}> + <SelectedAssetThemeProvider>{this.props.children}</SelectedAssetThemeProvider> + </Provider> + ); + } + + private readonly _flashErrorIfWrongNetwork = async (): Promise<void> => { + const msToShowError = 30000; // 30 seconds + const network = this._store.getState().network; + 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 424f35ecb..3e830ba00 100644 --- a/packages/instant/src/constants.ts +++ b/packages/instant/src/constants.ts @@ -2,6 +2,7 @@ import { BigNumber } from '@0x/utils'; export const BIG_NUMBER_ZERO = new BigNumber(0); export const ETH_DECIMALS = 18; export const DEFAULT_ZERO_EX_CONTAINER_SELECTOR = '#zeroExInstantContainer'; +export const INJECTED_DIV_ID = 'zeroExInstant'; export const WEB_3_WRAPPER_TRANSACTION_FAILED_ERROR_MSG_PREFIX = 'Transaction failed'; export const GWEI_IN_WEI = new BigNumber(1000000000); export const DEFAULT_GAS_PRICE = GWEI_IN_WEI.mul(6); diff --git a/packages/instant/src/data/asset_data_network_mapping.ts b/packages/instant/src/data/asset_data_network_mapping.ts index 45bfc2a02..a7bc6a967 100644 --- a/packages/instant/src/data/asset_data_network_mapping.ts +++ b/packages/instant/src/data/asset_data_network_mapping.ts @@ -56,4 +56,9 @@ export const assetDataNetworkMapping: AssetDataByNetwork[] = [ { [Network.Mainnet]: '0xf47261b000000000000000000000000008d32b0da63e2C3bcF8019c9c5d849d7a9d791e6', }, + // REP + { + [Network.Kovan]: '0xf47261b00000000000000000000000008cb3971b8eb709c14616bd556ff6683019e90d9c', + [Network.Mainnet]: '0xf47261b0000000000000000000000000e94327d07fc17907b4db788e5adf2ed424addff6', + }, ]; diff --git a/packages/instant/src/data/asset_meta_data_map.ts b/packages/instant/src/data/asset_meta_data_map.ts index d77298d78..d7cf2c0d8 100644 --- a/packages/instant/src/data/asset_meta_data_map.ts +++ b/packages/instant/src/data/asset_meta_data_map.ts @@ -65,4 +65,10 @@ export const assetMetaDataMap: ObjectMap<AssetMetaData> = { primaryColor: '#000', symbol: 'dentacoin', }, + '0xf47261b0000000000000000000000000e94327d07fc17907b4db788e5adf2ed424addff6': { + assetProxyId: AssetProxyId.ERC20, + decimals: 18, + primaryColor: '#512D80', + symbol: 'rep', + }, }; diff --git a/packages/instant/src/index.ts b/packages/instant/src/index.ts index 54059cdad..6e611dae8 100644 --- a/packages/instant/src/index.ts +++ b/packages/instant/src/index.ts @@ -1 +1,2 @@ export { ZeroExInstant, ZeroExInstantProps } from './components/zero_ex_instant'; +export { ZeroExInstantOverlay, ZeroExInstantOverlayProps } from './components/zero_ex_instant_overlay'; diff --git a/packages/instant/src/index.umd.ts b/packages/instant/src/index.umd.ts index b998abe95..81d59e092 100644 --- a/packages/instant/src/index.umd.ts +++ b/packages/instant/src/index.umd.ts @@ -2,26 +2,47 @@ import * as _ from 'lodash'; import * as React from 'react'; import * as ReactDOM from 'react-dom'; -import { DEFAULT_ZERO_EX_CONTAINER_SELECTOR } from './constants'; -import { ZeroExInstant, ZeroExInstantProps } from './index'; +import { DEFAULT_ZERO_EX_CONTAINER_SELECTOR, INJECTED_DIV_ID } from './constants'; +import { ZeroExInstantOverlay, ZeroExInstantOverlayProps } from './index'; import { assert } from './util/assert'; -export const render = (props: ZeroExInstantProps, selector: string = DEFAULT_ZERO_EX_CONTAINER_SELECTOR) => { +export const render = (props: ZeroExInstantOverlayProps, selector: string = DEFAULT_ZERO_EX_CONTAINER_SELECTOR) => { assert.isValidOrderSource('orderSource', props.orderSource); if (!_.isUndefined(props.defaultSelectedAssetData)) { assert.isHexString('defaultSelectedAssetData', props.defaultSelectedAssetData); } if (!_.isUndefined(props.additionalAssetMetaDataMap)) { - assert.isValidAssetMetaDataMap('additionalAssetMetaDataMap', props.additionalAssetMetaDataMap); + assert.isValidAssetMetaDataMap('props.additionalAssetMetaDataMap', props.additionalAssetMetaDataMap); } if (!_.isUndefined(props.defaultAssetBuyAmount)) { - assert.isNumber('defaultAssetBuyAmount', props.defaultAssetBuyAmount); + assert.isNumber('props.defaultAssetBuyAmount', props.defaultAssetBuyAmount); } if (!_.isUndefined(props.networkId)) { - assert.isNumber('networkId', props.networkId); + assert.isNumber('props.networkId', props.networkId); } if (!_.isUndefined(props.availableAssetDatas)) { assert.areValidAssetDatas('availableAssetDatas', props.availableAssetDatas); } - ReactDOM.render(React.createElement(ZeroExInstant, props), document.querySelector(selector)); + if (!_.isUndefined(props.onClose)) { + assert.isFunction('props.onClose', props.onClose); + } + if (!_.isUndefined(props.zIndex)) { + assert.isNumber('props.zIndex', props.zIndex); + } + const appendToIfExists = document.querySelector(selector); + assert.assert(!_.isNull(appendToIfExists), `Could not find div with selector: ${selector}`); + const appendTo = appendToIfExists as Element; + const injectedDiv = document.createElement('div'); + injectedDiv.setAttribute('id', INJECTED_DIV_ID); + appendTo.appendChild(injectedDiv); + const instantOverlayProps = { + ...props, + onClose: () => { + appendTo.removeChild(injectedDiv); + if (!_.isUndefined(props.onClose)) { + props.onClose(); + } + }, + }; + ReactDOM.render(React.createElement(ZeroExInstantOverlay, instantOverlayProps), injectedDiv); }; diff --git a/packages/instant/src/style/theme.ts b/packages/instant/src/style/theme.ts index 6575ff9f4..e81694620 100644 --- a/packages/instant/src/style/theme.ts +++ b/packages/instant/src/style/theme.ts @@ -29,5 +29,6 @@ export const theme: Theme = { }; export const transparentWhite = 'rgba(255,255,255,0.3)'; +export const overlayBlack = 'rgba(0, 0, 0, 0.6)'; export { styled, css, keyframes, ThemeProvider }; diff --git a/packages/website/ts/blockchain.ts b/packages/website/ts/blockchain.ts index 62e16dd1d..b43c41739 100644 --- a/packages/website/ts/blockchain.ts +++ b/packages/website/ts/blockchain.ts @@ -20,7 +20,7 @@ import { Web3ProviderEngine, } from '@0x/subproviders'; import { SignedOrder, Token as ZeroExToken } from '@0x/types'; -import { BigNumber, intervalUtils, logUtils, promisify } from '@0x/utils'; +import { BigNumber, intervalUtils, logUtils } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; import { BlockParam, LogWithDecodedArgs, Provider, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; import * as _ from 'lodash'; @@ -38,9 +38,9 @@ import { BlockchainErrs, ContractInstance, Fill, + InjectedProvider, InjectedProviderObservable, InjectedProviderUpdate, - InjectedWeb3, Providers, ProviderType, Side, @@ -83,6 +83,7 @@ export class Blockchain { private _ledgerSubprovider: LedgerSubprovider; private _defaultGasPrice: BigNumber; private _watchGasPriceIntervalId: NodeJS.Timer; + private _injectedProviderIfExists?: InjectedProvider; private static _getNameGivenProvider(provider: Provider): string { const providerType = utils.getProviderType(provider); const providerNameIfExists = providerToName[providerType]; @@ -91,48 +92,12 @@ export class Blockchain { } return providerNameIfExists; } - private static _getInjectedWeb3(): InjectedWeb3 { - const injectedWeb3IfExists = (window as any).web3; - // Our core assumptions about the injected web3 object is that it has the following - // properties and methods. - if ( - _.isUndefined(injectedWeb3IfExists) || - _.isUndefined(injectedWeb3IfExists.version) || - _.isUndefined(injectedWeb3IfExists.version.getNetwork) || - _.isUndefined(injectedWeb3IfExists.currentProvider) - ) { - return undefined; - } - return injectedWeb3IfExists; - } - private static async _getInjectedWeb3ProviderNetworkIdIfExistsAsync(): Promise<number | undefined> { - // Hack: We need to know the networkId the injectedWeb3 is connected to (if it is defined) in - // order to properly instantiate the web3Wrapper. Since we must use the async call, we cannot - // retrieve it from within the web3Wrapper constructor. This is and should remain the only - // call to a web3 instance outside of web3Wrapper in the entire dapp. - // In addition, if the user has an injectedWeb3 instance that is disconnected from a backing - // Ethereum node, this call will throw. We need to handle this case gracefully - const injectedWeb3IfExists = Blockchain._getInjectedWeb3(); - let networkIdIfExists: number; - if (!_.isUndefined(injectedWeb3IfExists)) { - try { - networkIdIfExists = _.parseInt( - await promisify<string>( - injectedWeb3IfExists.version.getNetwork.bind(injectedWeb3IfExists.version), - )(), - ); - } catch (err) { - // Ignore error and proceed with networkId undefined - } - } - return networkIdIfExists; - } private static async _getProviderAsync( - injectedWeb3: InjectedWeb3, - networkIdIfExists: number, + injectedProviderIfExists?: InjectedProvider, + networkIdIfExists?: number, shouldUserLedgerProvider: boolean = false, ): Promise<[Provider, LedgerSubprovider | undefined]> { - const doesInjectedWeb3Exist = !_.isUndefined(injectedWeb3); + const doesInjectedProviderExist = !_.isUndefined(injectedProviderIfExists); const isNetworkIdAvailable = !_.isUndefined(networkIdIfExists); const publicNodeUrlsIfExistsForNetworkId = configs.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkIdIfExists]; const isPublicNodeAvailableForNetworkId = !_.isUndefined(publicNodeUrlsIfExistsForNetworkId); @@ -156,16 +121,16 @@ export class Blockchain { provider.addProvider(new RedundantSubprovider(rpcSubproviders)); provider.start(); return [provider, ledgerSubprovider]; - } else if (doesInjectedWeb3Exist && isPublicNodeAvailableForNetworkId) { + } else if (doesInjectedProviderExist && isPublicNodeAvailableForNetworkId) { // We catch all requests involving a users account and send it to the injectedWeb3 // instance. All other requests go to the public hosted node. const provider = new Web3ProviderEngine(); - const providerName = this._getNameGivenProvider(injectedWeb3.currentProvider); + const providerName = this._getNameGivenProvider(injectedProviderIfExists); // Wrap Metamask in a compatability wrapper MetamaskSubprovider (to handle inconsistencies) const signerSubprovider = providerName === constants.PROVIDER_NAME_METAMASK - ? new MetamaskSubprovider(injectedWeb3.currentProvider) - : new SignerSubprovider(injectedWeb3.currentProvider); + ? new MetamaskSubprovider(injectedProviderIfExists) + : new SignerSubprovider(injectedProviderIfExists); provider.addProvider(signerSubprovider); provider.addProvider(new FilterSubprovider()); const rpcSubproviders = _.map(publicNodeUrlsIfExistsForNetworkId, publicNodeUrl => { @@ -174,9 +139,9 @@ export class Blockchain { provider.addProvider(new RedundantSubprovider(rpcSubproviders)); provider.start(); return [provider, undefined]; - } else if (doesInjectedWeb3Exist) { + } else if (doesInjectedProviderExist) { // Since no public node for this network, all requests go to injectedWeb3 instance - return [injectedWeb3.currentProvider, undefined]; + return [injectedProviderIfExists, undefined]; } else { // If no injectedWeb3 instance, all requests fallback to our public hosted mainnet/testnet node // We do this so that users can still browse the 0x Portal DApp even if they do not have web3 @@ -261,7 +226,7 @@ export class Blockchain { const shouldUserLedgerProvider = false; this._dispatcher.updateBlockchainIsLoaded(false); // We don't want to be out of sync with the network the injected provider declares. - const networkId = await Blockchain._getInjectedWeb3ProviderNetworkIdIfExistsAsync(); + const networkId = await this._getInjectedProviderNetworkIdIfExistsAsync(); await this._resetOrInitializeAsync(networkId, shouldPollUserAddress, shouldUserLedgerProvider); } public async setProxyAllowanceAsync(token: Token, amountInBaseUnits: BigNumber): Promise<void> { @@ -611,6 +576,45 @@ export class Blockchain { this._dispatcher.updateBlockchainIsLoaded(true); } + private async _getInjectedProviderIfExistsAsync(): Promise<InjectedProvider | undefined> { + if (!_.isUndefined(this._injectedProviderIfExists)) { + return this._injectedProviderIfExists; + } + let injectedProviderIfExists = (window as any).ethereum; + if (!_.isUndefined(injectedProviderIfExists)) { + if (!_.isUndefined(injectedProviderIfExists.enable)) { + try { + await injectedProviderIfExists.enable(); + } catch (err) { + errorReporter.report(err); + } + } + } else { + const injectedWeb3IfExists = (window as any).web3; + if (!_.isUndefined(injectedWeb3IfExists.currentProvider)) { + injectedProviderIfExists = injectedWeb3IfExists.currentProvider; + } else { + return undefined; + } + } + this._injectedProviderIfExists = injectedProviderIfExists; + return injectedProviderIfExists; + } + private async _getInjectedProviderNetworkIdIfExistsAsync(): Promise<number | undefined> { + // If the user has an injectedWeb3 instance that is disconnected from a backing + // Ethereum node, this call will throw. We need to handle this case gracefully + const injectedProviderIfExists = await this._getInjectedProviderIfExistsAsync(); + let networkIdIfExists: number; + if (!_.isUndefined(injectedProviderIfExists)) { + try { + const injectedWeb3Wrapper = new Web3Wrapper(injectedProviderIfExists); + networkIdIfExists = await injectedWeb3Wrapper.getNetworkIdAsync(); + } catch (err) { + // Ignore error and proceed with networkId undefined + } + } + return networkIdIfExists; + } private async _showEtherScanLinkAndAwaitTransactionMinedAsync( txHash: string, ): Promise<TransactionReceiptWithDecodedLogs> { @@ -804,17 +808,17 @@ export class Blockchain { } private async _onPageLoadInitFireAndForgetAsync(): Promise<void> { await utils.onPageLoadPromise; // wait for page to load - const networkIdIfExists = await Blockchain._getInjectedWeb3ProviderNetworkIdIfExistsAsync(); + const networkIdIfExists = await this._getInjectedProviderNetworkIdIfExistsAsync(); this.networkId = !_.isUndefined(networkIdIfExists) ? networkIdIfExists : constants.NETWORK_ID_MAINNET; - const injectedWeb3IfExists = Blockchain._getInjectedWeb3(); - if (!_.isUndefined(injectedWeb3IfExists) && !_.isUndefined(injectedWeb3IfExists.currentProvider)) { - const injectedProviderObservable = injectedWeb3IfExists.currentProvider.publicConfigStore; + const injectedProviderIfExists = await this._getInjectedProviderIfExistsAsync(); + if (!_.isUndefined(injectedProviderIfExists)) { + const injectedProviderObservable = injectedProviderIfExists.publicConfigStore; if (!_.isUndefined(injectedProviderObservable) && _.isUndefined(this._injectedProviderObservable)) { this._injectedProviderObservable = injectedProviderObservable; this._injectedProviderObservable.subscribe(this._injectedProviderUpdateHandler); } } - this._updateProviderName(injectedWeb3IfExists); + this._updateProviderName(injectedProviderIfExists); const shouldPollUserAddress = true; const shouldUseLedgerProvider = false; this._startWatchingGasPrice(); @@ -851,12 +855,14 @@ export class Blockchain { } this._dispatcher.updateUserWeiBalance(undefined); this.networkId = networkId; - const injectedWeb3IfExists = Blockchain._getInjectedWeb3(); + const injectedProviderIfExists = await this._getInjectedProviderIfExistsAsync(); const [provider, ledgerSubproviderIfExists] = await Blockchain._getProviderAsync( - injectedWeb3IfExists, + injectedProviderIfExists, networkId, shouldUserLedgerProvider, ); + this._web3Wrapper = new Web3Wrapper(provider); + this.networkId = await this._web3Wrapper.getNetworkIdAsync(); if (!_.isUndefined(this._contractWrappers)) { this._contractWrappers.unsubscribeAll(); } @@ -867,7 +873,6 @@ export class Blockchain { if (!_.isUndefined(this._blockchainWatcher)) { this._blockchainWatcher.destroy(); } - this._web3Wrapper = new Web3Wrapper(provider); this._blockchainWatcher = new BlockchainWatcher(this._dispatcher, this._web3Wrapper, shouldPollUserAddress); if (shouldUserLedgerProvider && !_.isUndefined(ledgerSubproviderIfExists)) { delete this._userAddressIfExists; @@ -879,7 +884,7 @@ export class Blockchain { const userAddresses = await this._web3Wrapper.getAvailableAddressesAsync(); this._userAddressIfExists = userAddresses[0]; this._dispatcher.updateUserAddress(this._userAddressIfExists); - if (!_.isUndefined(injectedWeb3IfExists)) { + if (!_.isUndefined(injectedProviderIfExists)) { this._dispatcher.updateProviderType(ProviderType.Injected); } await this.fetchTokenInformationAsync(); @@ -888,10 +893,10 @@ export class Blockchain { this._dispatcher.updateNetworkId(networkId); await this._rehydrateStoreWithContractEventsAsync(); } - private _updateProviderName(injectedWeb3IfExists: InjectedWeb3): void { - const doesInjectedWeb3Exist = !_.isUndefined(injectedWeb3IfExists); - const providerName = doesInjectedWeb3Exist - ? Blockchain._getNameGivenProvider(injectedWeb3IfExists.currentProvider) + private _updateProviderName(injectedProviderIfExists?: InjectedProvider): void { + const doesInjectedProviderExist = !_.isUndefined(injectedProviderIfExists); + const providerName = doesInjectedProviderExist + ? Blockchain._getNameGivenProvider(injectedProviderIfExists) : constants.PROVIDER_NAME_PUBLIC; this._dispatcher.updateInjectedProviderName(providerName); } diff --git a/packages/website/ts/types.ts b/packages/website/ts/types.ts index 22bfd2cb4..ce4b50a58 100644 --- a/packages/website/ts/types.ts +++ b/packages/website/ts/types.ts @@ -616,14 +616,6 @@ export interface InjectedProvider extends Provider { publicConfigStore?: InjectedProviderObservable; } -// Minimal expected interface for an injected web3 object -export interface InjectedWeb3 { - currentProvider: InjectedProvider; - version: { - getNetwork(cd: (err: Error, networkId: string) => void): void; - }; -} - export interface TutorialInfo { iconUrl: string; description: string; |