diff options
-rw-r--r-- | packages/instant/public/index.html | 10 | ||||
-rw-r--r-- | packages/instant/src/components/erc20_asset_amount_input.tsx | 37 | ||||
-rw-r--r-- | packages/instant/src/components/erc20_token_selector.tsx | 36 | ||||
-rw-r--r-- | packages/instant/src/components/ui/icon.tsx | 1 | ||||
-rw-r--r-- | packages/instant/src/components/zero_ex_instant_container.tsx | 3 | ||||
-rw-r--r-- | packages/instant/src/components/zero_ex_instant_provider.tsx | 36 | ||||
-rw-r--r-- | packages/instant/src/containers/available_erc20_token_selector.ts | 44 | ||||
-rw-r--r-- | packages/instant/src/containers/selected_erc20_asset_amount_input.ts | 20 | ||||
-rw-r--r-- | packages/instant/src/index.umd.ts | 9 | ||||
-rw-r--r-- | packages/instant/src/redux/actions.ts | 6 | ||||
-rw-r--r-- | packages/instant/src/redux/async_data.ts | 37 | ||||
-rw-r--r-- | packages/instant/src/redux/reducer.ts | 19 | ||||
-rw-r--r-- | packages/instant/src/util/assert.ts | 11 | ||||
-rw-r--r-- | packages/instant/src/util/asset.ts | 48 |
14 files changed, 251 insertions, 66 deletions
diff --git a/packages/instant/public/index.html b/packages/instant/public/index.html index 7580ab132..e1f8432be 100644 --- a/packages/instant/public/index.html +++ b/packages/instant/public/index.html @@ -72,16 +72,18 @@ ]; const queryParams = new Uri(window.location.search); const renderOptionsDefaults = { - liquiditySource: 'https://api.radarrelay.com/0x/v2/', + orderSource: 'https://api.radarrelay.com/0x/v2/', assetData: '0xf47261b0000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498', onClose: () => { console.log('0x Instant Closed') } } - const liquiditySourceOverride = queryParams.getQueryParamValue('liquiditySource'); + const orderSourceOverride = queryParams.getQueryParamValue('orderSource'); + const availableAssetDatasString = queryParams.getQueryParamValue('availableAssetDatas'); const renderOptionsOverrides = { - liquiditySource: liquiditySourceOverride === 'provided' ? providedOrders : liquiditySourceOverride, - assetData: queryParams.getQueryParamValue('assetData'), + orderSource: orderSourceOverride === 'provided' ? [providedOrder] : orderSourceOverride, networkId: +queryParams.getQueryParamValue('networkId') || undefined, defaultAssetBuyAmount: +queryParams.getQueryParamValue('defaultAssetBuyAmount') || undefined, + availableAssetDatas: availableAssetDatasString ? JSON.parse(availableAssetDatasString) : undefined, + defaultSelectedAssetData: queryParams.getQueryParamValue('defaultSelectedAssetData'), } const renderOptions = Object.assign({}, renderOptionsDefaults, removeUndefined(renderOptionsOverrides)); zeroExInstant.render(renderOptions); diff --git a/packages/instant/src/components/erc20_asset_amount_input.tsx b/packages/instant/src/components/erc20_asset_amount_input.tsx index b1fec6405..56f892328 100644 --- a/packages/instant/src/components/erc20_asset_amount_input.tsx +++ b/packages/instant/src/components/erc20_asset_amount_input.tsx @@ -19,6 +19,7 @@ export interface ERC20AssetAmountInputProps { startingFontSizePx: number; fontColor?: ColorOption; isDisabled: boolean; + numberOfAssetsAvailable?: number; } export interface ERC20AssetAmountInputState { @@ -47,6 +48,7 @@ export class ERC20AssetAmountInput extends React.Component<ERC20AssetAmountInput private readonly _renderContentForAsset = (asset: ERC20Asset): React.ReactNode => { const { onChange, ...rest } = this.props; const amountBorderBottom = this.props.isDisabled ? '' : `1px solid ${transparentWhite}`; + const onSymbolClick = this._generateSelectAssetClickHandler(); return ( <React.Fragment> <Container borderBottom={amountBorderBottom} display="inline-block"> @@ -59,7 +61,6 @@ export class ERC20AssetAmountInput extends React.Component<ERC20AssetAmountInput /> </Container> <Container - cursor="pointer" display="inline-block" marginLeft="8px" title={assetUtils.bestNameForAsset(asset, undefined)} @@ -69,7 +70,7 @@ export class ERC20AssetAmountInput extends React.Component<ERC20AssetAmountInput fontSize={`${this.state.currentFontSizePx}px`} fontColor={ColorOption.white} textTransform="uppercase" - onClick={this._handleSymbolClick} + onClick={onSymbolClick} > {assetUtils.formattedSymbolForAsset(asset)} </Text> @@ -80,6 +81,13 @@ export class ERC20AssetAmountInput extends React.Component<ERC20AssetAmountInput ); }; private readonly _renderBackupContent = (): React.ReactNode => { + const { numberOfAssetsAvailable } = this.props; + let text = 'Select Token'; + if (_.isUndefined(numberOfAssetsAvailable)) { + text = '...Loading'; + } else if (numberOfAssetsAvailable === 0) { + text = 'Assets Unavailable'; + } return ( <Flex> <Text @@ -87,17 +95,21 @@ export class ERC20AssetAmountInput extends React.Component<ERC20AssetAmountInput fontColor={ColorOption.white} opacity={0.7} fontWeight="500" - onClick={this._handleSymbolClick} + onClick={this._generateSelectAssetClickHandler()} > - Select Token + {text} </Text> {this._renderChevronIcon()} </Flex> ); }; private readonly _renderChevronIcon = (): React.ReactNode => { + const { numberOfAssetsAvailable } = this.props; + if (_.isUndefined(numberOfAssetsAvailable) || numberOfAssetsAvailable <= 1) { + return null; + } return ( - <Container marginLeft="5px" onClick={this._handleSymbolClick}> + <Container cursor="pointer" marginLeft="5px" onClick={this._generateSelectAssetClickHandler()}> <Icon icon="chevron" width={12} /> </Container> ); @@ -110,10 +122,19 @@ export class ERC20AssetAmountInput extends React.Component<ERC20AssetAmountInput currentFontSizePx: fontSizePx, }); }; - private readonly _handleSymbolClick = () => { - if (this.props.onSelectAssetClick) { - this.props.onSelectAssetClick(this.props.asset); + private readonly _generateSelectAssetClickHandler = (): (() => void) | undefined => { + // We don't want to allow opening the token selection panel if there are no assets. + // Since styles are inferred from the presence of a click handler, we want to return undefined + // instead of providing a noop. + const { numberOfAssetsAvailable, onSelectAssetClick } = this.props; + if ( + _.isUndefined(numberOfAssetsAvailable) || + numberOfAssetsAvailable <= 1 || + _.isUndefined(onSelectAssetClick) + ) { + return undefined; } + return () => onSelectAssetClick(this.props.asset); }; // For assets with symbols of different length, // start scaling the input at different character lengths diff --git a/packages/instant/src/components/erc20_token_selector.tsx b/packages/instant/src/components/erc20_token_selector.tsx new file mode 100644 index 000000000..12051b382 --- /dev/null +++ b/packages/instant/src/components/erc20_token_selector.tsx @@ -0,0 +1,36 @@ +import * as _ from 'lodash'; +import * as React from 'react'; + +import { ERC20Asset } from '../types'; + +import { Button, Container } from './ui'; + +export interface ERC20TokenSelectorProps { + tokens: ERC20Asset[]; + onTokenSelect: (token: ERC20Asset) => void; +} + +export const ERC20TokenSelector: React.StatelessComponent<ERC20TokenSelectorProps> = ({ tokens, onTokenSelect }) => ( + <Container> + {_.map(tokens, token => <TokenSelectorRow key={token.assetData} token={token} onClick={onTokenSelect} />)} + </Container> +); + +interface TokenSelectorRowProps { + token: ERC20Asset; + onClick: (token: ERC20Asset) => void; +} + +class TokenSelectorRow extends React.Component<TokenSelectorRowProps> { + public render(): React.ReactNode { + const { token } = this.props; + return ( + <Container> + <Button onClick={this._handleClick}>Select {token.metaData.symbol}</Button> + </Container> + ); + } + private readonly _handleClick = (): void => { + this.props.onClick(this.props.token); + }; +} diff --git a/packages/instant/src/components/ui/icon.tsx b/packages/instant/src/components/ui/icon.tsx index f12059cff..574cb26b7 100644 --- a/packages/instant/src/components/ui/icon.tsx +++ b/packages/instant/src/components/ui/icon.tsx @@ -104,7 +104,6 @@ export const Icon = styled(PlainIcon)` `; Icon.defaultProps = { - color: 'white', padding: '0em 0em', }; diff --git a/packages/instant/src/components/zero_ex_instant_container.tsx b/packages/instant/src/components/zero_ex_instant_container.tsx index 009ad9b2a..850beb49c 100644 --- a/packages/instant/src/components/zero_ex_instant_container.tsx +++ b/packages/instant/src/components/zero_ex_instant_container.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; +import { AvailableERC20TokenSelector } from '../containers/available_erc20_token_selector'; import { LatestBuyQuoteOrderDetails } from '../containers/latest_buy_quote_order_details'; import { LatestError } from '../containers/latest_error'; import { SelectedAssetBuyOrderStateButtons } from '../containers/selected_asset_buy_order_state_buttons'; @@ -45,7 +46,7 @@ export class ZeroExInstantContainer extends React.Component<ZeroExInstantContain animationState={this.state.tokenSelectionPanelAnimationState} onClose={this._handlePanelClose} > - Select Your Token + <AvailableERC20TokenSelector onTokenSelect={this._handlePanelClose} /> </SlidingPanel> </Container> </Container> diff --git a/packages/instant/src/components/zero_ex_instant_provider.tsx b/packages/instant/src/components/zero_ex_instant_provider.tsx index 8c1025723..8552a7fb5 100644 --- a/packages/instant/src/components/zero_ex_instant_provider.tsx +++ b/packages/instant/src/components/zero_ex_instant_provider.tsx @@ -17,25 +17,28 @@ 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 { - // TODO: Change API when we allow the selection of different assetDatas - assetData: string; - liquiditySource: string | SignedOrder[]; + orderSource: string | SignedOrder[]; } export interface ZeroExInstantProviderOptionalProps { - defaultAssetBuyAmount?: number; + 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. @@ -44,14 +47,14 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider networkId, }; let assetBuyer; - if (_.isString(props.liquiditySource)) { + if (_.isString(props.orderSource)) { assetBuyer = AssetBuyer.getAssetBuyerForStandardRelayerAPIUrl( provider, - props.liquiditySource, + props.orderSource, assetBuyerOptions, ); } else { - assetBuyer = AssetBuyer.getAssetBuyerForProvidedOrders(provider, props.liquiditySource, assetBuyerOptions); + assetBuyer = AssetBuyer.getAssetBuyerForProvidedOrders(provider, props.orderSource, assetBuyerOptions); } const completeAssetMetaDataMap = { ...props.additionalAssetMetaDataMap, @@ -61,10 +64,19 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider ...state, assetBuyer, network: networkId, - selectedAsset: assetUtils.createAssetFromAssetData(props.assetData, completeAssetMetaDataMap, 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; @@ -76,8 +88,14 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider } public componentDidMount(): void { + const state = this._store.getState(); // tslint:disable-next-line:no-floating-promises - asyncData.fetchAndDispatchToStore(this._store); + 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 diff --git a/packages/instant/src/containers/available_erc20_token_selector.ts b/packages/instant/src/containers/available_erc20_token_selector.ts new file mode 100644 index 000000000..bb194cd6b --- /dev/null +++ b/packages/instant/src/containers/available_erc20_token_selector.ts @@ -0,0 +1,44 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; + +import { State } from '../redux/reducer'; +import { ERC20Asset } from '../types'; +import { assetUtils } from '../util/asset'; + +import { ERC20TokenSelector } from '../components/erc20_token_selector'; +import { Action, actions } from '../redux/actions'; + +export interface AvailableERC20TokenSelectorProps { + onTokenSelect?: (token: ERC20Asset) => void; +} + +interface ConnectedState { + tokens: ERC20Asset[]; +} + +interface ConnectedDispatch { + onTokenSelect: (token: ERC20Asset) => void; +} + +const mapStateToProps = (state: State, _ownProps: AvailableERC20TokenSelectorProps): ConnectedState => ({ + tokens: assetUtils.getERC20AssetsFromAssets(state.availableAssets || []), +}); + +const mapDispatchToProps = ( + dispatch: Dispatch<Action>, + ownProps: AvailableERC20TokenSelectorProps, +): ConnectedDispatch => ({ + onTokenSelect: (token: ERC20Asset) => { + if (ownProps.onTokenSelect) { + ownProps.onTokenSelect(token); + } + dispatch(actions.updateSelectedAsset(token)); + }, +}); + +export const AvailableERC20TokenSelector: React.ComponentClass<AvailableERC20TokenSelectorProps> = connect( + mapStateToProps, + mapDispatchToProps, +)(ERC20TokenSelector); 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 217d603d2..64b6f4b3b 100644 --- a/packages/instant/src/containers/selected_erc20_asset_amount_input.ts +++ b/packages/instant/src/containers/selected_erc20_asset_amount_input.ts @@ -27,6 +27,7 @@ interface ConnectedState { value?: BigNumberInput; asset?: ERC20Asset; isDisabled: boolean; + numberOfAssetsAvailable?: number; } interface ConnectedDispatch { @@ -38,6 +39,7 @@ interface ConnectedProps { asset?: ERC20Asset; onChange: (value?: BigNumberInput, asset?: ERC20Asset) => void; isDisabled: boolean; + numberOfAssetsAvailable?: number; } type FinalProps = ConnectedProps & SelectedERC20AssetAmountInputProps; @@ -46,20 +48,17 @@ const mapStateToProps = (state: State, _ownProps: SelectedERC20AssetAmountInputP const processState = state.buyOrderState.processState; const isEnabled = processState === OrderProcessState.NONE || processState === OrderProcessState.FAILURE; const isDisabled = !isEnabled; - - const selectedAsset = state.selectedAsset; - if (_.isUndefined(selectedAsset) || selectedAsset.metaData.assetProxyId !== AssetProxyId.ERC20) { - return { - value: state.selectedAssetAmount, - isDisabled, - }; - } - + 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; return { assetBuyer: state.assetBuyer, value: state.selectedAssetAmount, - asset: selectedAsset as ERC20Asset, + asset: selectedAsset, isDisabled, + numberOfAssetsAvailable, }; }; @@ -138,6 +137,7 @@ const mergeProps = ( connectedDispatch.updateBuyQuote(connectedState.assetBuyer, value, asset); }, isDisabled: connectedState.isDisabled, + numberOfAssetsAvailable: connectedState.numberOfAssetsAvailable, }; }; diff --git a/packages/instant/src/index.umd.ts b/packages/instant/src/index.umd.ts index b12e65485..81d59e092 100644 --- a/packages/instant/src/index.umd.ts +++ b/packages/instant/src/index.umd.ts @@ -7,8 +7,10 @@ import { ZeroExInstantOverlay, ZeroExInstantOverlayProps } from './index'; import { assert } from './util/assert'; export const render = (props: ZeroExInstantOverlayProps, selector: string = DEFAULT_ZERO_EX_CONTAINER_SELECTOR) => { - assert.isHexString('props.assetData', props.assetData); - assert.isValidLiquiditySource('props.liquiditySource', props.liquiditySource); + assert.isValidOrderSource('orderSource', props.orderSource); + if (!_.isUndefined(props.defaultSelectedAssetData)) { + assert.isHexString('defaultSelectedAssetData', props.defaultSelectedAssetData); + } if (!_.isUndefined(props.additionalAssetMetaDataMap)) { assert.isValidAssetMetaDataMap('props.additionalAssetMetaDataMap', props.additionalAssetMetaDataMap); } @@ -18,6 +20,9 @@ export const render = (props: ZeroExInstantOverlayProps, selector: string = DEFA if (!_.isUndefined(props.networkId)) { assert.isNumber('props.networkId', props.networkId); } + if (!_.isUndefined(props.availableAssetDatas)) { + assert.areValidAssetDatas('availableAssetDatas', props.availableAssetDatas); + } if (!_.isUndefined(props.onClose)) { assert.isFunction('props.onClose', props.onClose); } diff --git a/packages/instant/src/redux/actions.ts b/packages/instant/src/redux/actions.ts index bfae68e2b..813c5e819 100644 --- a/packages/instant/src/redux/actions.ts +++ b/packages/instant/src/redux/actions.ts @@ -4,7 +4,7 @@ import * as _ from 'lodash'; import { BigNumberInput } from '../util/big_number_input'; -import { ActionsUnion, OrderState } from '../types'; +import { ActionsUnion, Asset, OrderState } from '../types'; export interface PlainAction<T extends string> { type: T; @@ -28,6 +28,7 @@ export enum ActionTypes { UPDATE_BUY_ORDER_STATE = 'UPDATE_BUY_ORDER_STATE', UPDATE_LATEST_BUY_QUOTE = 'UPDATE_LATEST_BUY_QUOTE', UPDATE_SELECTED_ASSET = 'UPDATE_SELECTED_ASSET', + SET_AVAILABLE_ASSETS = 'SET_AVAILABLE_ASSETS', SET_QUOTE_REQUEST_STATE_PENDING = 'SET_QUOTE_REQUEST_STATE_PENDING', SET_QUOTE_REQUEST_STATE_FAILURE = 'SET_QUOTE_REQUEST_STATE_FAILURE', SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE', @@ -42,7 +43,8 @@ export const actions = { createAction(ActionTypes.UPDATE_SELECTED_ASSET_AMOUNT, amount), updateBuyOrderState: (orderState: OrderState) => createAction(ActionTypes.UPDATE_BUY_ORDER_STATE, orderState), updateLatestBuyQuote: (buyQuote?: BuyQuote) => createAction(ActionTypes.UPDATE_LATEST_BUY_QUOTE, buyQuote), - updateSelectedAsset: (assetData?: string) => createAction(ActionTypes.UPDATE_SELECTED_ASSET, assetData), + updateSelectedAsset: (asset: Asset) => createAction(ActionTypes.UPDATE_SELECTED_ASSET, asset), + setAvailableAssets: (availableAssets: Asset[]) => createAction(ActionTypes.SET_AVAILABLE_ASSETS, availableAssets), setQuoteRequestStatePending: () => createAction(ActionTypes.SET_QUOTE_REQUEST_STATE_PENDING), setQuoteRequestStateFailure: () => createAction(ActionTypes.SET_QUOTE_REQUEST_STATE_FAILURE), setErrorMessage: (errorMessage: string) => createAction(ActionTypes.SET_ERROR_MESSAGE, errorMessage), diff --git a/packages/instant/src/redux/async_data.ts b/packages/instant/src/redux/async_data.ts index 4ed89bdc3..716558409 100644 --- a/packages/instant/src/redux/async_data.ts +++ b/packages/instant/src/redux/async_data.ts @@ -1,22 +1,37 @@ +import * as _ from 'lodash'; + import { BIG_NUMBER_ZERO } from '../constants'; +import { assetUtils } from '../util/asset'; import { coinbaseApi } from '../util/coinbase_api'; +import { errorFlasher } from '../util/error_flasher'; -import { ActionTypes } from './actions'; - +import { actions } from './actions'; import { Store } from './store'; export const asyncData = { - fetchAndDispatchToStore: async (store: Store) => { - let ethUsdPrice = BIG_NUMBER_ZERO; + fetchEthPriceAndDispatchToStore: async (store: Store) => { try { - ethUsdPrice = await coinbaseApi.getEthUsdPrice(); + const ethUsdPrice = await coinbaseApi.getEthUsdPrice(); + store.dispatch(actions.updateEthUsdPrice(ethUsdPrice)); } catch (e) { - // ignore - } finally { - store.dispatch({ - type: ActionTypes.UPDATE_ETH_USD_PRICE, - data: ethUsdPrice, - }); + const errorMessage = 'Error fetching ETH/USD price'; + errorFlasher.flashNewErrorMessage(store.dispatch, errorMessage); + store.dispatch(actions.updateEthUsdPrice(BIG_NUMBER_ZERO)); + } + }, + 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 = 'Error fetching available 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 dd9403052..20d927561 100644 --- a/packages/instant/src/redux/reducer.ts +++ b/packages/instant/src/redux/reducer.ts @@ -14,7 +14,6 @@ import { OrderProcessState, OrderState, } from '../types'; -import { assetUtils } from '../util/asset'; import { BigNumberInput } from '../util/big_number_input'; import { Action, ActionTypes } from './actions'; @@ -24,6 +23,7 @@ export interface State { assetBuyer?: AssetBuyer; assetMetaDataMap: ObjectMap<AssetMetaData>; selectedAsset?: Asset; + availableAssets?: Asset[]; selectedAssetAmount?: BigNumberInput; buyOrderState: OrderState; ethUsdPrice?: BigNumber; @@ -36,6 +36,7 @@ export interface State { export const INITIAL_STATE: State = { network: Network.Mainnet, selectedAssetAmount: undefined, + availableAssets: undefined, assetMetaDataMap, buyOrderState: { processState: OrderProcessState.NONE }, ethUsdPrice: undefined, @@ -106,18 +107,9 @@ export const reducer = (state: State = INITIAL_STATE, action: Action): State => latestErrorDisplayStatus: 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, + selectedAsset: action.data, }; case ActionTypes.RESET_AMOUNT: return { @@ -127,6 +119,11 @@ export const reducer = (state: State = INITIAL_STATE, action: Action): State => buyOrderState: { processState: OrderProcessState.NONE }, selectedAssetAmount: undefined, }; + case ActionTypes.SET_AVAILABLE_ASSETS: + return { + ...state, + availableAssets: action.data, + }; default: return state; } diff --git a/packages/instant/src/util/assert.ts b/packages/instant/src/util/assert.ts index 584d3d4b1..7c07215bf 100644 --- a/packages/instant/src/util/assert.ts +++ b/packages/instant/src/util/assert.ts @@ -8,12 +8,15 @@ import { AssetMetaData } from '../types'; export const assert = { ...sharedAssert, - isValidLiquiditySource(variableName: string, liquiditySource: string | SignedOrder[]): void { - if (_.isString(liquiditySource)) { - sharedAssert.isUri(variableName, liquiditySource); + isValidOrderSource(variableName: string, orderSource: string | SignedOrder[]): void { + if (_.isString(orderSource)) { + sharedAssert.isUri(variableName, orderSource); return; } - sharedAssert.doesConformToSchema(variableName, liquiditySource, schemas.signedOrdersSchema); + sharedAssert.doesConformToSchema(variableName, orderSource, schemas.signedOrdersSchema); + }, + areValidAssetDatas(variableName: string, assetDatas: string[]): void { + _.forEach(assetDatas, (assetData, index) => assert.isHexString(`${variableName}[${index}]`, assetData)); }, isValidAssetMetaDataMap(variableName: string, metaDataMap: ObjectMap<AssetMetaData>): void { _.forEach(metaDataMap, (metaData, assetData) => { diff --git a/packages/instant/src/util/asset.ts b/packages/instant/src/util/asset.ts index 630103c7b..2efaadbd2 100644 --- a/packages/instant/src/util/asset.ts +++ b/packages/instant/src/util/asset.ts @@ -5,7 +5,31 @@ import { assetDataNetworkMapping } from '../data/asset_data_network_mapping'; import { Asset, AssetMetaData, ERC20Asset, Network, ZeroExInstantError } from '../types'; export const assetUtils = { - createAssetFromAssetData: ( + createAssetsFromAssetDatas: ( + assetDatas: string[], + assetMetaDataMap: ObjectMap<AssetMetaData>, + network: Network, + ): Asset[] => { + const arrayOfAssetOrUndefined = _.map(assetDatas, assetData => + assetUtils.createAssetFromAssetDataIfExists(assetData, assetMetaDataMap, network), + ); + return _.compact(arrayOfAssetOrUndefined); + }, + createAssetFromAssetDataIfExists: ( + assetData: string, + assetMetaDataMap: ObjectMap<AssetMetaData>, + network: Network, + ): Asset | undefined => { + const metaData = assetUtils.getMetaDataIfExists(assetData, assetMetaDataMap, network); + if (_.isUndefined(metaData)) { + return; + } + return { + assetData, + metaData, + }; + }, + createAssetFromAssetDataOrThrow: ( assetData: string, assetMetaDataMap: ObjectMap<AssetMetaData>, network: Network, @@ -16,6 +40,17 @@ export const assetUtils = { }; }, getMetaDataOrThrow: (assetData: string, metaDataMap: ObjectMap<AssetMetaData>, network: Network): AssetMetaData => { + const metaDataIfExists = assetUtils.getMetaDataIfExists(assetData, metaDataMap, network); + if (_.isUndefined(metaDataIfExists)) { + throw new Error(ZeroExInstantError.AssetMetaDataNotAvailable); + } + return metaDataIfExists; + }, + getMetaDataIfExists: ( + assetData: string, + metaDataMap: ObjectMap<AssetMetaData>, + network: Network, + ): AssetMetaData | undefined => { let mainnetAssetData: string | undefined = assetData; if (network !== Network.Mainnet) { const mainnetAssetDataIfExists = assetUtils.getAssociatedAssetDataIfExists(assetData, network); @@ -24,11 +59,11 @@ export const assetUtils = { mainnetAssetData = mainnetAssetDataIfExists || assetData; } if (_.isUndefined(mainnetAssetData)) { - throw new Error(ZeroExInstantError.AssetMetaDataNotAvailable); + return; } const metaData = metaDataMap[mainnetAssetData]; if (_.isUndefined(metaData)) { - throw new Error(ZeroExInstantError.AssetMetaDataNotAvailable); + return; } return metaData; }, @@ -63,4 +98,11 @@ export const assetUtils = { } return assetDataGroupIfExists[Network.Mainnet]; }, + getERC20AssetsFromAssets: (assets: Asset[]): ERC20Asset[] => { + const erc20sOrUndefined = _.map( + assets, + asset => (asset.metaData.assetProxyId === AssetProxyId.ERC20 ? (asset as ERC20Asset) : undefined), + ); + return _.compact(erc20sOrUndefined); + }, }; |