diff options
Diffstat (limited to 'packages/instant')
25 files changed, 373 insertions, 129 deletions
diff --git a/packages/instant/package.json b/packages/instant/package.json index d7ec85b2f..1a6e9d2e9 100644 --- a/packages/instant/package.json +++ b/packages/instant/package.json @@ -45,6 +45,7 @@ "homepage": "https://github.com/0xProject/0x-monorepo/packages/instant/README.md", "dependencies": { "@0x/asset-buyer": "^2.1.0", + "@0x/order-utils": "^2.0.0", "@0x/types": "^1.2.0", "@0x/typescript-typings": "^3.0.3", "@0x/utils": "^2.0.3", diff --git a/packages/instant/public/index.html b/packages/instant/public/index.html index fb041745e..14555fc64 100644 --- a/packages/instant/public/index.html +++ b/packages/instant/public/index.html @@ -25,7 +25,8 @@ <div id="zeroExInstantContainer"></div> <script> zeroExInstant.render({ - + liquiditySource: 'https://api.radarrelay.com/0x/v2/', + assetData: '0xf47261b0000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498', }); </script> </body> diff --git a/packages/instant/src/components/asset_amount_input.tsx b/packages/instant/src/components/asset_amount_input.tsx index 730e6396f..c03ef1cf3 100644 --- a/packages/instant/src/components/asset_amount_input.tsx +++ b/packages/instant/src/components/asset_amount_input.tsx @@ -2,17 +2,18 @@ import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; import * as React from 'react'; -import { assetDataUtil } from '../util/asset_data'; - import { ColorOption } from '../style/theme'; +import { ERC20Asset } from '../types'; +import { assetUtils } from '../util/asset'; import { util } from '../util/util'; import { AmountInput, AmountInputProps } from './amount_input'; import { Container, Text } from './ui'; +// Asset amounts only apply to ERC20 assets export interface AssetAmountInputProps extends AmountInputProps { - assetData?: string; - onChange: (value?: BigNumber, assetData?: string) => void; + asset?: ERC20Asset; + onChange: (value?: BigNumber, asset?: ERC20Asset) => void; } export class AssetAmountInput extends React.Component<AssetAmountInputProps> { @@ -20,19 +21,19 @@ export class AssetAmountInput extends React.Component<AssetAmountInputProps> { onChange: util.boundNoop, }; public render(): React.ReactNode { - const { assetData, onChange, ...rest } = this.props; + const { asset, onChange, ...rest } = this.props; return ( <Container> <AmountInput {...rest} onChange={this._handleChange} /> <Container display="inline-block" marginLeft="10px"> <Text fontSize={rest.fontSize} fontColor={ColorOption.white} textTransform="uppercase"> - {assetDataUtil.bestNameForAsset(this.props.assetData, '???')} + {assetUtils.bestNameForAsset(asset)} </Text> </Container> </Container> ); } private readonly _handleChange = (value?: BigNumber): void => { - this.props.onChange(value, this.props.assetData); + this.props.onChange(value, this.props.asset); }; } diff --git a/packages/instant/src/components/buy_button.tsx b/packages/instant/src/components/buy_button.tsx index d2a8bd07a..adc32f071 100644 --- a/packages/instant/src/components/buy_button.tsx +++ b/packages/instant/src/components/buy_button.tsx @@ -1,9 +1,8 @@ -import { BuyQuote } from '@0x/asset-buyer'; +import { AssetBuyer, BuyQuote } from '@0x/asset-buyer'; import * as _ from 'lodash'; import * as React from 'react'; import { ColorOption } from '../style/theme'; -import { assetBuyer } from '../util/asset_buyer'; import { util } from '../util/util'; import { web3Wrapper } from '../util/web3_wrapper'; @@ -11,6 +10,7 @@ import { Button, Container, Text } from './ui'; export interface BuyButtonProps { buyQuote?: BuyQuote; + assetBuyer?: AssetBuyer; onClick: (buyQuote: BuyQuote) => void; onBuySuccess: (buyQuote: BuyQuote, txnHash: string) => void; onBuyFailure: (buyQuote: BuyQuote, tnxHash?: string) => void; @@ -24,7 +24,7 @@ export class BuyButton extends React.Component<BuyButtonProps> { onBuyFailure: util.boundNoop, }; public render(): React.ReactNode { - const shouldDisableButton = _.isUndefined(this.props.buyQuote); + const shouldDisableButton = _.isUndefined(this.props.buyQuote) || _.isUndefined(this.props.assetBuyer); return ( <Container padding="20px" width="100%"> <Button width="100%" onClick={this._handleClick} isDisabled={shouldDisableButton}> @@ -37,13 +37,13 @@ export class BuyButton extends React.Component<BuyButtonProps> { } private readonly _handleClick = async () => { // The button is disabled when there is no buy quote anyway. - if (_.isUndefined(this.props.buyQuote)) { + if (_.isUndefined(this.props.buyQuote) || _.isUndefined(this.props.assetBuyer)) { return; } this.props.onClick(this.props.buyQuote); let txnHash; try { - txnHash = await assetBuyer.executeBuyQuoteAsync(this.props.buyQuote); + txnHash = await this.props.assetBuyer.executeBuyQuoteAsync(this.props.buyQuote); await web3Wrapper.awaitTransactionSuccessAsync(txnHash); this.props.onBuySuccess(this.props.buyQuote, txnHash); } catch { diff --git a/packages/instant/src/components/zero_ex_instant.tsx b/packages/instant/src/components/zero_ex_instant.tsx index f6472e811..ffa5a8250 100644 --- a/packages/instant/src/components/zero_ex_instant.tsx +++ b/packages/instant/src/components/zero_ex_instant.tsx @@ -1,23 +1,75 @@ +import { AssetBuyer } from '@0x/asset-buyer'; +import { ObjectMap } from '@0x/types'; 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 { store } from '../redux/store'; +import { INITIAL_STATE, State } from '../redux/reducer'; +import { store, Store } from '../redux/store'; import { fonts } from '../style/fonts'; -import { theme, ThemeProvider } from '../style/theme'; +import { AssetMetaData, Network } from '../types'; +import { assetUtils } from '../util/asset'; +import { getProvider } from '../util/provider'; import { ZeroExInstantContainer } from './zero_ex_instant_container'; fonts.include(); -// tslint:disable-next-line:no-floating-promises -asyncData.fetchAndDispatchToStore(); - -export interface ZeroExInstantProps {} - -export const ZeroExInstant: React.StatelessComponent<ZeroExInstantProps> = () => ( - <Provider store={store}> - <ThemeProvider theme={theme}> - <ZeroExInstantContainer /> - </ThemeProvider> - </Provider> -); + +export type ZeroExInstantProps = ZeroExInstantRequiredProps & Partial<ZeroExInstantOptionalProps>; + +export interface ZeroExInstantRequiredProps { + // TODO: Change API when we allow the selection of different assetDatas + assetData: string; + // TODO: Allow for a function that returns orders + liquiditySource: string; +} + +export interface ZeroExInstantOptionalProps { + additionalAssetMetaDataMap: ObjectMap<AssetMetaData>; + network: Network; +} + +export class ZeroExInstant extends React.Component<ZeroExInstantProps> { + private readonly _store: Store; + private static _mergeInitialStateWithProps(props: ZeroExInstantProps, state: State = INITIAL_STATE): State { + // Create merged object such that properties in props override default settings + const optionalPropsWithDefaults: ZeroExInstantOptionalProps = { + additionalAssetMetaDataMap: props.additionalAssetMetaDataMap || {}, + network: props.network || state.network, + }; + const { network } = optionalPropsWithDefaults; + // TODO: Provider needs to not be hard-coded to injected web3. + const assetBuyer = AssetBuyer.getAssetBuyerForStandardRelayerAPIUrl(getProvider(), props.liquiditySource, { + networkId: network, + }); + const completeAssetMetaDataMap = { + ...props.additionalAssetMetaDataMap, + ...state.assetMetaDataMap, + }; + const storeStateFromProps: State = { + ...state, + assetBuyer, + network, + selectedAsset: assetUtils.createAssetFromAssetData(props.assetData, completeAssetMetaDataMap, network), + assetMetaDataMap: completeAssetMetaDataMap, + }; + return storeStateFromProps; + } + constructor(props: ZeroExInstantProps) { + super(props); + this._store = store.create(ZeroExInstant._mergeInitialStateWithProps(this.props, INITIAL_STATE)); + // tslint:disable-next-line:no-floating-promises + asyncData.fetchAndDispatchToStore(this._store); + } + + public render(): React.ReactNode { + return ( + <Provider store={this._store}> + <SelectedAssetThemeProvider> + <ZeroExInstantContainer /> + </SelectedAssetThemeProvider> + </Provider> + ); + } +} diff --git a/packages/instant/src/constants.ts b/packages/instant/src/constants.ts index 5d5341f9c..31491c80a 100644 --- a/packages/instant/src/constants.ts +++ b/packages/instant/src/constants.ts @@ -1,6 +1,4 @@ import { BigNumber } from '@0x/utils'; export const BIG_NUMBER_ZERO = new BigNumber(0); -export const sraApiUrl = 'https://api.radarrelay.com/0x/v2/'; -export const zrxAssetData = '0xf47261b0000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498'; -export const zrxDecimals = 18; export const ethDecimals = 18; +export const DEFAULT_ZERO_EX_CONTAINER_SELECTOR = '#zeroExInstantContainer'; diff --git a/packages/instant/src/containers/latest_error.tsx b/packages/instant/src/containers/latest_error.tsx index 413cf16ad..b75ec00aa 100644 --- a/packages/instant/src/containers/latest_error.tsx +++ b/packages/instant/src/containers/latest_error.tsx @@ -4,11 +4,11 @@ import { connect } from 'react-redux'; import { SlidingError } from '../components/sliding_error'; import { State } from '../redux/reducer'; -import { DisplayStatus } from '../types'; +import { Asset, DisplayStatus } from '../types'; import { errorUtil } from '../util/error'; export interface LatestErrorComponentProps { - assetData?: string; + asset?: Asset; latestError?: any; slidingDirection: 'down' | 'up'; } @@ -17,18 +17,18 @@ export const LatestErrorComponent: React.StatelessComponent<LatestErrorComponent if (!props.latestError) { return <div />; } - const { icon, message } = errorUtil.errorDescription(props.latestError, props.assetData); + const { icon, message } = errorUtil.errorDescription(props.latestError, props.asset); return <SlidingError direction={props.slidingDirection} icon={icon} message={message} />; }; interface ConnectedState { - assetData?: string; + asset?: Asset; latestError?: any; slidingDirection: 'down' | 'up'; } export interface LatestErrorProps {} const mapStateToProps = (state: State, _ownProps: LatestErrorProps): ConnectedState => ({ - assetData: state.selectedAssetData, + asset: state.selectedAsset, latestError: state.latestError, slidingDirection: state.latestErrorDisplay === DisplayStatus.Present ? 'up' : 'down', }); diff --git a/packages/instant/src/containers/selected_asset_amount_input.ts b/packages/instant/src/containers/selected_asset_amount_input.ts index e55c8b991..0d847cf02 100644 --- a/packages/instant/src/containers/selected_asset_amount_input.ts +++ b/packages/instant/src/containers/selected_asset_amount_input.ts @@ -1,4 +1,5 @@ -import { BuyQuote } from '@0x/asset-buyer'; +import { AssetBuyer, BuyQuote } from '@0x/asset-buyer'; +import { AssetProxyId } from '@0x/types'; import { BigNumber } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; import * as _ from 'lodash'; @@ -6,12 +7,10 @@ import * as React from 'react'; import { connect } from 'react-redux'; import { Dispatch } from 'redux'; -import { zrxDecimals } from '../constants'; import { Action, actions } from '../redux/actions'; import { State } from '../redux/reducer'; import { ColorOption } from '../style/theme'; -import { AsyncProcessState } from '../types'; -import { assetBuyer } from '../util/asset_buyer'; +import { AsyncProcessState, ERC20Asset } from '../types'; import { errorUtil } from '../util/error'; import { AssetAmountInput } from '../components/asset_amount_input'; @@ -22,33 +21,52 @@ export interface SelectedAssetAmountInputProps { } interface ConnectedState { + assetBuyer?: AssetBuyer; value?: BigNumber; - assetData?: string; + asset?: ERC20Asset; } interface ConnectedDispatch { - onChange: (value?: BigNumber, assetData?: string) => void; + updateBuyQuote: (assetBuyer?: AssetBuyer, value?: BigNumber, asset?: ERC20Asset) => void; } -const mapStateToProps = (state: State, _ownProps: SelectedAssetAmountInputProps): ConnectedState => ({ - value: state.selectedAssetAmount, - assetData: state.selectedAssetData, -}); +interface ConnectedProps { + value?: BigNumber; + asset?: ERC20Asset; + onChange: (value?: BigNumber, asset?: ERC20Asset) => void; +} + +type FinalProps = ConnectedProps & SelectedAssetAmountInputProps; + +const mapStateToProps = (state: State, _ownProps: SelectedAssetAmountInputProps): ConnectedState => { + const selectedAsset = state.selectedAsset; + if (_.isUndefined(selectedAsset) || selectedAsset.metaData.assetProxyId !== AssetProxyId.ERC20) { + return { + value: state.selectedAssetAmount, + }; + } + return { + assetBuyer: state.assetBuyer, + value: state.selectedAssetAmount, + asset: selectedAsset as ERC20Asset, + }; +}; const updateBuyQuoteAsync = async ( + assetBuyer: AssetBuyer, dispatch: Dispatch<Action>, - assetData: string, + asset: ERC20Asset, assetAmount: BigNumber, ): Promise<void> => { // get a new buy quote. - const baseUnitValue = Web3Wrapper.toBaseUnitAmount(assetAmount, zrxDecimals); + const baseUnitValue = Web3Wrapper.toBaseUnitAmount(assetAmount, asset.metaData.decimals); // mark quote as pending dispatch(actions.setQuoteRequestStatePending()); let newBuyQuote: BuyQuote | undefined; try { - newBuyQuote = await assetBuyer.getBuyQuoteAsync(assetData, baseUnitValue); + newBuyQuote = await assetBuyer.getBuyQuoteAsync(asset.assetData, baseUnitValue); } catch (error) { dispatch(actions.setQuoteRequestStateFailure()); errorUtil.errorFlasher.flashNewError(dispatch, error); @@ -66,7 +84,7 @@ const mapDispatchToProps = ( dispatch: Dispatch<Action>, _ownProps: SelectedAssetAmountInputProps, ): ConnectedDispatch => ({ - onChange: (value, assetData) => { + updateBuyQuote: (assetBuyer, value, asset) => { // Update the input dispatch(actions.updateSelectedAssetAmount(value)); // invalidate the last buy quote. @@ -74,16 +92,32 @@ const mapDispatchToProps = ( // reset our buy state dispatch(actions.updateBuyOrderState(AsyncProcessState.NONE)); - if (!_.isUndefined(value) && !_.isUndefined(assetData)) { + if (!_.isUndefined(value) && !_.isUndefined(asset) && !_.isUndefined(assetBuyer)) { // even if it's debounced, give them the illusion it's loading dispatch(actions.setQuoteRequestStatePending()); // tslint:disable-next-line:no-floating-promises - debouncedUpdateBuyQuoteAsync(dispatch, assetData, value); + debouncedUpdateBuyQuoteAsync(assetBuyer, dispatch, asset, value); } }, }); +const mergeProps = ( + connectedState: ConnectedState, + connectedDispatch: ConnectedDispatch, + ownProps: SelectedAssetAmountInputProps, +): FinalProps => { + return { + ...ownProps, + asset: connectedState.asset, + value: connectedState.value, + onChange: (value, asset) => { + connectedDispatch.updateBuyQuote(connectedState.assetBuyer, value, asset); + }, + }; +}; + export const SelectedAssetAmountInput: React.ComponentClass<SelectedAssetAmountInputProps> = connect( mapStateToProps, mapDispatchToProps, + mergeProps, )(AssetAmountInput); diff --git a/packages/instant/src/containers/selected_asset_buy_button.ts b/packages/instant/src/containers/selected_asset_buy_button.ts index 4d3315b1a..8189a5377 100644 --- a/packages/instant/src/containers/selected_asset_buy_button.ts +++ b/packages/instant/src/containers/selected_asset_buy_button.ts @@ -1,4 +1,4 @@ -import { BuyQuote } from '@0x/asset-buyer'; +import { AssetBuyer, BuyQuote } from '@0x/asset-buyer'; import * as _ from 'lodash'; import * as React from 'react'; import { connect } from 'react-redux'; @@ -13,6 +13,7 @@ import { BuyButton } from '../components/buy_button'; export interface SelectedAssetBuyButtonProps {} interface ConnectedState { + assetBuyer?: AssetBuyer; text: string; buyQuote?: BuyQuote; } @@ -39,6 +40,7 @@ const textForState = (state: AsyncProcessState): string => { }; const mapStateToProps = (state: State, _ownProps: SelectedAssetBuyButtonProps): ConnectedState => ({ + assetBuyer: state.assetBuyer, text: textForState(state.buyOrderState), buyQuote: state.latestBuyQuote, }); diff --git a/packages/instant/src/containers/selected_asset_theme_provider.ts b/packages/instant/src/containers/selected_asset_theme_provider.ts new file mode 100644 index 000000000..6e6b83d73 --- /dev/null +++ b/packages/instant/src/containers/selected_asset_theme_provider.ts @@ -0,0 +1,32 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import { connect } from 'react-redux'; + +import { State } from '../redux/reducer'; +import { Theme, theme as defaultTheme, ThemeProvider } from '../style/theme'; +import { Asset } from '../types'; + +export interface SelectedAssetThemeProviderProps {} + +interface ConnectedState { + theme: Theme; +} + +const getTheme = (asset?: Asset): Theme => { + if (!_.isUndefined(asset) && !_.isUndefined(asset.metaData.primaryColor)) { + return { + ...defaultTheme, + primaryColor: asset.metaData.primaryColor, + }; + } + return defaultTheme; +}; + +const mapStateToProps = (state: State, _ownProps: SelectedAssetThemeProviderProps): ConnectedState => { + const theme = getTheme(state.selectedAsset); + return { theme }; +}; + +export const SelectedAssetThemeProvider: React.ComponentClass<SelectedAssetThemeProviderProps> = connect( + mapStateToProps, +)(ThemeProvider); diff --git a/packages/instant/src/data/asset_data_network_mapping.ts b/packages/instant/src/data/asset_data_network_mapping.ts new file mode 100644 index 000000000..e8ccbf011 --- /dev/null +++ b/packages/instant/src/data/asset_data_network_mapping.ts @@ -0,0 +1,15 @@ +import * as _ from 'lodash'; + +import { Network } from '../types'; + +interface AssetDataByNetwork { + [Network.Kovan]?: string; + [Network.Mainnet]?: string; +} + +export const assetDataNetworkMapping: AssetDataByNetwork[] = [ + { + [Network.Mainnet]: '0xf47261b0000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498', + [Network.Kovan]: '0xf47261b00000000000000000000000002002d3812f58e35f0ea1ffbf80a75a38c32175fa', + }, +]; diff --git a/packages/instant/src/data/asset_meta_data.ts b/packages/instant/src/data/asset_meta_data_map.ts index ae0d32e4b..3a820a0c4 100644 --- a/packages/instant/src/data/asset_meta_data.ts +++ b/packages/instant/src/data/asset_meta_data_map.ts @@ -1,12 +1,11 @@ import { AssetProxyId, ObjectMap } from '@0x/types'; -import { zrxAssetData } from '../constants'; import { AssetMetaData } from '../types'; // Map from assetData string to AssetMetaData object // TODO: import this from somewhere else. -export const assetMetaData: ObjectMap<AssetMetaData> = { - [zrxAssetData]: { +export const assetMetaDataMap: ObjectMap<AssetMetaData> = { + '0xf47261b0000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498': { assetProxyId: AssetProxyId.ERC20, decimals: 18, primaryColor: 'rgb(54, 50, 60)', diff --git a/packages/instant/src/index.umd.ts b/packages/instant/src/index.umd.ts index d4eca177d..f648b37f2 100644 --- a/packages/instant/src/index.umd.ts +++ b/packages/instant/src/index.umd.ts @@ -1,10 +1,9 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; -import { ZeroExInstant } from './index'; +import { DEFAULT_ZERO_EX_CONTAINER_SELECTOR } from './constants'; +import { ZeroExInstant, ZeroExInstantProps } from './index'; -export interface ZeroExInstantOptions {} - -export const render = (props: ZeroExInstantOptions, selector: string = '#zeroExInstantContainer') => { +export const render = (props: ZeroExInstantProps, selector: string = DEFAULT_ZERO_EX_CONTAINER_SELECTOR) => { ReactDOM.render(React.createElement(ZeroExInstant, props), document.querySelector(selector)); }; diff --git a/packages/instant/src/redux/actions.ts b/packages/instant/src/redux/actions.ts index e52a79e76..2c430ff83 100644 --- a/packages/instant/src/redux/actions.ts +++ b/packages/instant/src/redux/actions.ts @@ -25,6 +25,7 @@ export enum ActionTypes { UPDATE_SELECTED_ASSET_AMOUNT = 'UPDATE_SELECTED_ASSET_AMOUNT', UPDATE_SELECTED_ASSET_BUY_STATE = 'UPDATE_SELECTED_ASSET_BUY_STATE', UPDATE_LATEST_BUY_QUOTE = 'UPDATE_LATEST_BUY_QUOTE', + UPDATE_SELECTED_ASSET = 'UPDATE_SELECTED_ASSET', SET_QUOTE_REQUEST_STATE_PENDING = 'SET_QUOTE_REQUEST_STATE_PENDING', SET_QUOTE_REQUEST_STATE_FAILURE = 'SET_QUOTE_REQUEST_STATE_FAILURE', SET_ERROR = 'SET_ERROR', @@ -38,6 +39,7 @@ export const actions = { updateBuyOrderState: (buyState: AsyncProcessState) => createAction(ActionTypes.UPDATE_SELECTED_ASSET_BUY_STATE, buyState), updateLatestBuyQuote: (buyQuote?: BuyQuote) => createAction(ActionTypes.UPDATE_LATEST_BUY_QUOTE, buyQuote), + updateSelectedAsset: (assetData?: string) => createAction(ActionTypes.UPDATE_SELECTED_ASSET, assetData), setQuoteRequestStatePending: () => createAction(ActionTypes.SET_QUOTE_REQUEST_STATE_PENDING), setQuoteRequestStateFailure: () => createAction(ActionTypes.SET_QUOTE_REQUEST_STATE_FAILURE), setError: (error?: any) => createAction(ActionTypes.SET_ERROR, error), diff --git a/packages/instant/src/redux/async_data.ts b/packages/instant/src/redux/async_data.ts index 348838307..4ed89bdc3 100644 --- a/packages/instant/src/redux/async_data.ts +++ b/packages/instant/src/redux/async_data.ts @@ -3,10 +3,10 @@ import { coinbaseApi } from '../util/coinbase_api'; import { ActionTypes } from './actions'; -import { store } from './store'; +import { Store } from './store'; export const asyncData = { - fetchAndDispatchToStore: async () => { + fetchAndDispatchToStore: async (store: Store) => { let ethUsdPrice = BIG_NUMBER_ZERO; try { ethUsdPrice = await coinbaseApi.getEthUsdPrice(); diff --git a/packages/instant/src/redux/reducer.ts b/packages/instant/src/redux/reducer.ts index 2d50dd4b9..1538621a5 100644 --- a/packages/instant/src/redux/reducer.ts +++ b/packages/instant/src/redux/reducer.ts @@ -1,14 +1,19 @@ -import { BuyQuote } from '@0x/asset-buyer'; +import { AssetBuyer, BuyQuote } from '@0x/asset-buyer'; +import { ObjectMap } from '@0x/types'; import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; -import { zrxAssetData } from '../constants'; -import { AsyncProcessState, DisplayStatus } from '../types'; +import { assetMetaDataMap } from '../data/asset_meta_data_map'; +import { Asset, AssetMetaData, AsyncProcessState, DisplayStatus, Network } from '../types'; +import { assetUtils } from '../util/asset'; import { Action, ActionTypes } from './actions'; export interface State { - selectedAssetData?: string; + network: Network; + assetBuyer?: AssetBuyer; + assetMetaDataMap: ObjectMap<AssetMetaData>; + selectedAsset?: Asset; selectedAssetAmount?: BigNumber; buyOrderState: AsyncProcessState; ethUsdPrice?: BigNumber; @@ -19,9 +24,9 @@ export interface State { } export const INITIAL_STATE: State = { - // TODO: Remove hardcoded zrxAssetData - selectedAssetData: zrxAssetData, + network: Network.Mainnet, selectedAssetAmount: undefined, + assetMetaDataMap, buyOrderState: AsyncProcessState.NONE, ethUsdPrice: undefined, latestBuyQuote: undefined, @@ -82,6 +87,20 @@ export const reducer = (state: State = INITIAL_STATE, action: Action): State => latestError: undefined, latestErrorDisplay: DisplayStatus.Hidden, }; + case ActionTypes.UPDATE_SELECTED_ASSET: + const newSelectedAssetData = action.data; + let newSelectedAsset: Asset | undefined; + if (!_.isUndefined(newSelectedAssetData)) { + newSelectedAsset = assetUtils.createAssetFromAssetData( + newSelectedAssetData, + state.assetMetaDataMap, + state.network, + ); + } + return { + ...state, + selectedAsset: newSelectedAsset, + }; default: return state; } diff --git a/packages/instant/src/redux/store.ts b/packages/instant/src/redux/store.ts index b9ce9c0c1..01deb8690 100644 --- a/packages/instant/src/redux/store.ts +++ b/packages/instant/src/redux/store.ts @@ -4,4 +4,10 @@ import { devToolsEnhancer } from 'redux-devtools-extension/developmentOnly'; import { reducer, State } from './reducer'; -export const store: ReduxStore<State> = createStore(reducer, devToolsEnhancer({})); +export type Store = ReduxStore<State>; + +export const store = { + create: (state: State): Store => { + return createStore(reducer, state, devToolsEnhancer({})); + }, +}; diff --git a/packages/instant/src/types.ts b/packages/instant/src/types.ts index 013ada27b..c340623ad 100644 --- a/packages/instant/src/types.ts +++ b/packages/instant/src/types.ts @@ -26,12 +26,32 @@ export interface ERC20AssetMetaData { export interface ERC721AssetMetaData { assetProxyId: AssetProxyId.ERC721; name: string; + representationUrl?: string; primaryColor?: string; } export type AssetMetaData = ERC20AssetMetaData | ERC721AssetMetaData; +export interface ERC20Asset { + assetData: string; + metaData: ERC20AssetMetaData; +} + +export interface ERC721Asset { + assetData: string; + metaData: ERC721AssetMetaData; +} + +export interface Asset { + assetData: string; + metaData: AssetMetaData; +} + export enum Network { Kovan = 42, Mainnet = 1, } + +export enum ZeroExInstantError { + AssetMetaDataNotAvailable = 'ASSET_META_DATA_NOT_AVAILABLE', +} diff --git a/packages/instant/src/util/asset.ts b/packages/instant/src/util/asset.ts new file mode 100644 index 000000000..4e3b2b946 --- /dev/null +++ b/packages/instant/src/util/asset.ts @@ -0,0 +1,53 @@ +import { AssetProxyId, ObjectMap } from '@0x/types'; +import * as _ from 'lodash'; + +import { assetDataNetworkMapping } from '../data/asset_data_network_mapping'; +import { Asset, AssetMetaData, Network, ZeroExInstantError } from '../types'; + +export const assetUtils = { + createAssetFromAssetData: ( + assetData: string, + assetMetaDataMap: ObjectMap<AssetMetaData>, + network: Network, + ): Asset => { + return { + assetData, + metaData: assetUtils.getMetaDataOrThrow(assetData, assetMetaDataMap, network), + }; + }, + getMetaDataOrThrow: (assetData: string, metaDataMap: ObjectMap<AssetMetaData>, network: Network): AssetMetaData => { + let mainnetAssetData: string | undefined = assetData; + if (network !== Network.Mainnet) { + mainnetAssetData = assetUtils.getAssociatedAssetDataIfExists(assetData, network); + } + if (_.isUndefined(mainnetAssetData)) { + throw new Error(ZeroExInstantError.AssetMetaDataNotAvailable); + } + const metaData = metaDataMap[mainnetAssetData]; + if (_.isUndefined(metaData)) { + throw new Error(ZeroExInstantError.AssetMetaDataNotAvailable); + } + return metaData; + }, + bestNameForAsset: (asset?: Asset, defaultName: string = '???'): string => { + if (_.isUndefined(asset)) { + return defaultName; + } + const metaData = asset.metaData; + switch (metaData.assetProxyId) { + case AssetProxyId.ERC20: + return metaData.symbol.toUpperCase(); + case AssetProxyId.ERC721: + return metaData.name; + default: + return defaultName; + } + }, + getAssociatedAssetDataIfExists: (assetData: string, network: Network): string | undefined => { + const assetDataGroupIfExists = _.find(assetDataNetworkMapping, value => value[network] === assetData); + if (_.isUndefined(assetDataGroupIfExists)) { + return; + } + return assetDataGroupIfExists[Network.Mainnet]; + }, +}; diff --git a/packages/instant/src/util/asset_buyer.ts b/packages/instant/src/util/asset_buyer.ts deleted file mode 100644 index 6855fbcab..000000000 --- a/packages/instant/src/util/asset_buyer.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { AssetBuyer } from '@0x/asset-buyer'; - -import { sraApiUrl } from '../constants'; - -import { getProvider } from './provider'; - -const provider = getProvider(); - -export const assetBuyer = AssetBuyer.getAssetBuyerForStandardRelayerAPIUrl(provider, sraApiUrl); diff --git a/packages/instant/src/util/asset_data.ts b/packages/instant/src/util/asset_data.ts deleted file mode 100644 index fea2e2b19..000000000 --- a/packages/instant/src/util/asset_data.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as _ from 'lodash'; - -import { AssetProxyId } from '@0x/types'; - -import { assetMetaData } from '../data/asset_meta_data'; - -export const assetDataUtil = { - bestNameForAsset: (assetData: string | undefined, defaultString: string) => { - if (_.isUndefined(assetData)) { - return defaultString; - } - const metaData = assetMetaData[assetData]; - if (_.isUndefined(metaData)) { - return defaultString; - } - if (metaData.assetProxyId === AssetProxyId.ERC20) { - return metaData.symbol.toUpperCase(); - } - return defaultString; - }, -}; diff --git a/packages/instant/src/util/error.ts b/packages/instant/src/util/error.ts index c9b13ef83..40fd24c7e 100644 --- a/packages/instant/src/util/error.ts +++ b/packages/instant/src/util/error.ts @@ -2,7 +2,9 @@ import { AssetBuyerError } from '@0x/asset-buyer'; import { Dispatch } from 'redux'; import { Action, actions } from '../redux/actions'; -import { assetDataUtil } from '../util/asset_data'; +import { Asset } from '../types'; + +import { assetUtils } from './asset'; class ErrorFlasher { private _timeoutId?: number; @@ -27,12 +29,12 @@ class ErrorFlasher { } } -const humanReadableMessageForError = (error: Error, assetData?: string): string | undefined => { +const humanReadableMessageForError = (error: Error, asset?: Asset): string | undefined => { const hasInsufficientLiquidity = error.message === AssetBuyerError.InsufficientAssetLiquidity || error.message === AssetBuyerError.InsufficientZrxLiquidity; if (hasInsufficientLiquidity) { - const assetName = assetDataUtil.bestNameForAsset(assetData, 'of this asset'); + const assetName = assetUtils.bestNameForAsset(asset, 'of this asset'); return `Not enough ${assetName} available`; } @@ -40,7 +42,7 @@ const humanReadableMessageForError = (error: Error, assetData?: string): string error.message === AssetBuyerError.StandardRelayerApiError || error.message.startsWith(AssetBuyerError.AssetUnavailable) ) { - const assetName = assetDataUtil.bestNameForAsset(assetData, 'This asset'); + const assetName = assetUtils.bestNameForAsset(asset, 'This asset'); return `${assetName} is currently unavailable`; } @@ -49,10 +51,10 @@ const humanReadableMessageForError = (error: Error, assetData?: string): string export const errorUtil = { errorFlasher: new ErrorFlasher(), - errorDescription: (error?: any, assetData?: string): { icon: string; message: string } => { + errorDescription: (error?: any, asset?: Asset): { icon: string; message: string } => { let bestMessage: string | undefined; if (error instanceof Error) { - bestMessage = humanReadableMessageForError(error, assetData); + bestMessage = humanReadableMessageForError(error, asset); } return { icon: '😢', diff --git a/packages/instant/test/util/asset.test.ts b/packages/instant/test/util/asset.test.ts new file mode 100644 index 000000000..c7db7eba7 --- /dev/null +++ b/packages/instant/test/util/asset.test.ts @@ -0,0 +1,47 @@ +import { AssetProxyId, ObjectMap } from '@0x/types'; + +import { Asset, AssetMetaData, ERC20AssetMetaData, Network, ZeroExInstantError } from '../../src/types'; +import { assetUtils } from '../../src/util/asset'; + +const ZRX_ASSET_DATA = '0xf47261b0000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498'; +const ZRX_ASSET_DATA_KOVAN = '0xf47261b00000000000000000000000002002d3812f58e35f0ea1ffbf80a75a38c32175fa'; +const ZRX_META_DATA: ERC20AssetMetaData = { + assetProxyId: AssetProxyId.ERC20, + symbol: 'zrx', + decimals: 18, +}; +const ZRX_ASSET: Asset = { + assetData: ZRX_ASSET_DATA, + metaData: ZRX_META_DATA, +}; +const META_DATA_MAP: ObjectMap<AssetMetaData> = { + [ZRX_ASSET_DATA]: ZRX_META_DATA, +}; + +describe('assetDataUtil', () => { + describe('bestNameForAsset', () => { + it('should return default string if assetData is undefined', () => { + expect(assetUtils.bestNameForAsset(undefined, 'xyz')).toEqual('xyz'); + }); + it('should return ZRX for ZRX assetData', () => { + expect(assetUtils.bestNameForAsset(ZRX_ASSET, 'mah default')).toEqual('ZRX'); + }); + }); + describe('getMetaDataOrThrow', () => { + it('should return the metaData for the supplied mainnet asset data', () => { + expect(assetUtils.getMetaDataOrThrow(ZRX_ASSET_DATA, META_DATA_MAP, Network.Mainnet)).toEqual( + ZRX_META_DATA, + ); + }); + it('should return the metaData for the supplied non-mainnet asset data', () => { + expect(assetUtils.getMetaDataOrThrow(ZRX_ASSET_DATA_KOVAN, META_DATA_MAP, Network.Kovan)).toEqual( + ZRX_META_DATA, + ); + }); + it('should throw if the metaData for the asset is not available', () => { + expect(() => + assetUtils.getMetaDataOrThrow('asset data we dont have', META_DATA_MAP, Network.Mainnet), + ).toThrowError(ZeroExInstantError.AssetMetaDataNotAvailable); + }); + }); +}); diff --git a/packages/instant/test/util/asset_data.test.ts b/packages/instant/test/util/asset_data.test.ts deleted file mode 100644 index cf247142a..000000000 --- a/packages/instant/test/util/asset_data.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { assetDataUtil } from '../../src/util/asset_data'; - -const ZRX_ASSET_DATA = '0xf47261b0000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498'; - -describe('assetDataUtil', () => { - describe('bestNameForAsset', () => { - it('should return default string if assetData is undefined', () => { - expect(assetDataUtil.bestNameForAsset(undefined, 'xyz')).toEqual('xyz'); - }); - it('should return default string if assetData isnt found', () => { - expect(assetDataUtil.bestNameForAsset('fake', 'mah default')).toEqual('mah default'); - }); - it('should return ZRX for ZRX assetData', () => { - expect(assetDataUtil.bestNameForAsset(ZRX_ASSET_DATA, 'mah default')).toEqual('ZRX'); - }); - }); -}); diff --git a/packages/instant/test/util/error.test.ts b/packages/instant/test/util/error.test.ts index 78b742f06..90e9c5fb4 100644 --- a/packages/instant/test/util/error.test.ts +++ b/packages/instant/test/util/error.test.ts @@ -1,14 +1,24 @@ import { AssetBuyerError } from '@0x/asset-buyer'; +import { AssetProxyId } from '@0x/types'; +import { Asset } from '../../src/types'; import { errorUtil } from '../../src/util/error'; const ZRX_ASSET_DATA = '0xf47261b0000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498'; +const ZRX_ASSET: Asset = { + assetData: ZRX_ASSET_DATA, + metaData: { + assetProxyId: AssetProxyId.ERC20, + symbol: 'zrx', + decimals: 18, + }, +}; describe('errorUtil', () => { describe('errorFlasher', () => { it('should return error and asset name for InsufficientAssetLiquidity', () => { const insufficientAssetError = new Error(AssetBuyerError.InsufficientAssetLiquidity); - expect(errorUtil.errorDescription(insufficientAssetError, ZRX_ASSET_DATA).message).toEqual( + expect(errorUtil.errorDescription(insufficientAssetError, ZRX_ASSET).message).toEqual( 'Not enough ZRX available', ); }); @@ -20,27 +30,25 @@ describe('errorUtil', () => { }); it('should return asset name for InsufficientAssetLiquidity', () => { const insufficientZrxError = new Error(AssetBuyerError.InsufficientZrxLiquidity); - expect(errorUtil.errorDescription(insufficientZrxError, ZRX_ASSET_DATA).message).toEqual( + expect(errorUtil.errorDescription(insufficientZrxError, ZRX_ASSET).message).toEqual( 'Not enough ZRX available', ); }); it('should return unavailable error and asset name for StandardRelayerApiError', () => { const standardRelayerError = new Error(AssetBuyerError.StandardRelayerApiError); - expect(errorUtil.errorDescription(standardRelayerError, ZRX_ASSET_DATA).message).toEqual( + expect(errorUtil.errorDescription(standardRelayerError, ZRX_ASSET).message).toEqual( 'ZRX is currently unavailable', ); }); it('should return error for AssetUnavailable error', () => { - const assetUnavailableError = new Error( - `${AssetBuyerError.AssetUnavailable}: For assetData ${ZRX_ASSET_DATA}`, - ); - expect(errorUtil.errorDescription(assetUnavailableError, ZRX_ASSET_DATA).message).toEqual( + const assetUnavailableError = new Error(`${AssetBuyerError.AssetUnavailable}: For assetData ${ZRX_ASSET}`); + expect(errorUtil.errorDescription(assetUnavailableError, ZRX_ASSET).message).toEqual( 'ZRX is currently unavailable', ); }); it('should return default for AssetUnavailable error', () => { const assetUnavailableError = new Error(`${AssetBuyerError.AssetUnavailable}: For assetData xyz`); - expect(errorUtil.errorDescription(assetUnavailableError, 'xyz').message).toEqual( + expect(errorUtil.errorDescription(assetUnavailableError, undefined).message).toEqual( 'This asset is currently unavailable', ); }); |