Merge pull request #1159 from 0xProject/feature/instant/beta-render-et-al

[instant] Pass in liquiditySource, assetData and other settings from render
This commit is contained in:
Francesco Agosti 2018-10-23 17:00:54 -07:00 committed by GitHub
commit 4a72dc6c6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 377 additions and 133 deletions

View File

@ -45,6 +45,7 @@
"homepage": "https://github.com/0xProject/0x-monorepo/packages/instant/README.md", "homepage": "https://github.com/0xProject/0x-monorepo/packages/instant/README.md",
"dependencies": { "dependencies": {
"@0x/asset-buyer": "^2.1.0", "@0x/asset-buyer": "^2.1.0",
"@0x/order-utils": "^2.0.0",
"@0x/types": "^1.2.0", "@0x/types": "^1.2.0",
"@0x/typescript-typings": "^3.0.3", "@0x/typescript-typings": "^3.0.3",
"@0x/utils": "^2.0.3", "@0x/utils": "^2.0.3",

View File

@ -25,7 +25,8 @@
<div id="zeroExInstantContainer"></div> <div id="zeroExInstantContainer"></div>
<script> <script>
zeroExInstant.render({ zeroExInstant.render({
liquiditySource: 'https://api.radarrelay.com/0x/v2/',
assetData: '0xf47261b0000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498',
}); });
</script> </script>
</body> </body>

View File

@ -2,17 +2,18 @@ import { BigNumber } from '@0x/utils';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as React from 'react'; import * as React from 'react';
import { assetDataUtil } from '../util/asset_data';
import { ColorOption } from '../style/theme'; import { ColorOption } from '../style/theme';
import { ERC20Asset } from '../types';
import { assetUtils } from '../util/asset';
import { util } from '../util/util'; import { util } from '../util/util';
import { AmountInput, AmountInputProps } from './amount_input'; import { AmountInput, AmountInputProps } from './amount_input';
import { Container, Text } from './ui'; import { Container, Text } from './ui';
// Asset amounts only apply to ERC20 assets
export interface AssetAmountInputProps extends AmountInputProps { export interface AssetAmountInputProps extends AmountInputProps {
assetData?: string; asset?: ERC20Asset;
onChange: (value?: BigNumber, assetData?: string) => void; onChange: (value?: BigNumber, asset?: ERC20Asset) => void;
} }
export class AssetAmountInput extends React.Component<AssetAmountInputProps> { export class AssetAmountInput extends React.Component<AssetAmountInputProps> {
@ -20,19 +21,19 @@ export class AssetAmountInput extends React.Component<AssetAmountInputProps> {
onChange: util.boundNoop, onChange: util.boundNoop,
}; };
public render(): React.ReactNode { public render(): React.ReactNode {
const { assetData, onChange, ...rest } = this.props; const { asset, onChange, ...rest } = this.props;
return ( return (
<Container> <Container>
<AmountInput {...rest} onChange={this._handleChange} /> <AmountInput {...rest} onChange={this._handleChange} />
<Container display="inline-block" marginLeft="10px"> <Container display="inline-block" marginLeft="10px">
<Text fontSize={rest.fontSize} fontColor={ColorOption.white} textTransform="uppercase"> <Text fontSize={rest.fontSize} fontColor={ColorOption.white} textTransform="uppercase">
{assetDataUtil.bestNameForAsset(this.props.assetData, '???')} {assetUtils.bestNameForAsset(asset)}
</Text> </Text>
</Container> </Container>
</Container> </Container>
); );
} }
private readonly _handleChange = (value?: BigNumber): void => { private readonly _handleChange = (value?: BigNumber): void => {
this.props.onChange(value, this.props.assetData); this.props.onChange(value, this.props.asset);
}; };
} }

View File

@ -1,9 +1,8 @@
import { BuyQuote } from '@0x/asset-buyer'; import { AssetBuyer, BuyQuote } from '@0x/asset-buyer';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as React from 'react'; import * as React from 'react';
import { ColorOption } from '../style/theme'; import { ColorOption } from '../style/theme';
import { assetBuyer } from '../util/asset_buyer';
import { util } from '../util/util'; import { util } from '../util/util';
import { web3Wrapper } from '../util/web3_wrapper'; import { web3Wrapper } from '../util/web3_wrapper';
@ -11,6 +10,7 @@ import { Button, Container, Text } from './ui';
export interface BuyButtonProps { export interface BuyButtonProps {
buyQuote?: BuyQuote; buyQuote?: BuyQuote;
assetBuyer?: AssetBuyer;
onClick: (buyQuote: BuyQuote) => void; onClick: (buyQuote: BuyQuote) => void;
onBuySuccess: (buyQuote: BuyQuote, txnHash: string) => void; onBuySuccess: (buyQuote: BuyQuote, txnHash: string) => void;
onBuyFailure: (buyQuote: BuyQuote, tnxHash?: string) => void; onBuyFailure: (buyQuote: BuyQuote, tnxHash?: string) => void;
@ -24,7 +24,7 @@ export class BuyButton extends React.Component<BuyButtonProps> {
onBuyFailure: util.boundNoop, onBuyFailure: util.boundNoop,
}; };
public render(): React.ReactNode { public render(): React.ReactNode {
const shouldDisableButton = _.isUndefined(this.props.buyQuote); const shouldDisableButton = _.isUndefined(this.props.buyQuote) || _.isUndefined(this.props.assetBuyer);
return ( return (
<Container padding="20px" width="100%"> <Container padding="20px" width="100%">
<Button width="100%" onClick={this._handleClick} isDisabled={shouldDisableButton}> <Button width="100%" onClick={this._handleClick} isDisabled={shouldDisableButton}>
@ -37,13 +37,13 @@ export class BuyButton extends React.Component<BuyButtonProps> {
} }
private readonly _handleClick = async () => { private readonly _handleClick = async () => {
// The button is disabled when there is no buy quote anyway. // 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; return;
} }
this.props.onClick(this.props.buyQuote); this.props.onClick(this.props.buyQuote);
let txnHash; let txnHash;
try { try {
txnHash = await assetBuyer.executeBuyQuoteAsync(this.props.buyQuote); txnHash = await this.props.assetBuyer.executeBuyQuoteAsync(this.props.buyQuote);
await web3Wrapper.awaitTransactionSuccessAsync(txnHash); await web3Wrapper.awaitTransactionSuccessAsync(txnHash);
this.props.onBuySuccess(this.props.buyQuote, txnHash); this.props.onBuySuccess(this.props.buyQuote, txnHash);
} catch { } catch {

View File

@ -1,23 +1,75 @@
import { AssetBuyer } from '@0x/asset-buyer';
import { ObjectMap } from '@0x/types';
import * as React from 'react'; import * as React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { SelectedAssetThemeProvider } from '../containers/selected_asset_theme_provider';
import { asyncData } from '../redux/async_data'; 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 { 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'; import { ZeroExInstantContainer } from './zero_ex_instant_container';
fonts.include(); fonts.include();
// tslint:disable-next-line:no-floating-promises
asyncData.fetchAndDispatchToStore();
export interface ZeroExInstantProps {} export type ZeroExInstantProps = ZeroExInstantRequiredProps & Partial<ZeroExInstantOptionalProps>;
export const ZeroExInstant: React.StatelessComponent<ZeroExInstantProps> = () => ( export interface ZeroExInstantRequiredProps {
<Provider store={store}> // TODO: Change API when we allow the selection of different assetDatas
<ThemeProvider theme={theme}> assetData: string;
<ZeroExInstantContainer /> // TODO: Allow for a function that returns orders
</ThemeProvider> liquiditySource: string;
</Provider> }
);
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>
);
}
}

View File

@ -1,6 +1,4 @@
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
export const BIG_NUMBER_ZERO = new BigNumber(0); 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 ethDecimals = 18;
export const DEFAULT_ZERO_EX_CONTAINER_SELECTOR = '#zeroExInstantContainer';

View File

@ -4,11 +4,11 @@ import { connect } from 'react-redux';
import { SlidingError } from '../components/sliding_error'; import { SlidingError } from '../components/sliding_error';
import { State } from '../redux/reducer'; import { State } from '../redux/reducer';
import { DisplayStatus } from '../types'; import { Asset, DisplayStatus } from '../types';
import { errorUtil } from '../util/error'; import { errorUtil } from '../util/error';
export interface LatestErrorComponentProps { export interface LatestErrorComponentProps {
assetData?: string; asset?: Asset;
latestError?: any; latestError?: any;
slidingDirection: 'down' | 'up'; slidingDirection: 'down' | 'up';
} }
@ -17,18 +17,18 @@ export const LatestErrorComponent: React.StatelessComponent<LatestErrorComponent
if (!props.latestError) { if (!props.latestError) {
return <div />; 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} />; return <SlidingError direction={props.slidingDirection} icon={icon} message={message} />;
}; };
interface ConnectedState { interface ConnectedState {
assetData?: string; asset?: Asset;
latestError?: any; latestError?: any;
slidingDirection: 'down' | 'up'; slidingDirection: 'down' | 'up';
} }
export interface LatestErrorProps {} export interface LatestErrorProps {}
const mapStateToProps = (state: State, _ownProps: LatestErrorProps): ConnectedState => ({ const mapStateToProps = (state: State, _ownProps: LatestErrorProps): ConnectedState => ({
assetData: state.selectedAssetData, asset: state.selectedAsset,
latestError: state.latestError, latestError: state.latestError,
slidingDirection: state.latestErrorDisplay === DisplayStatus.Present ? 'up' : 'down', slidingDirection: state.latestErrorDisplay === DisplayStatus.Present ? 'up' : 'down',
}); });

View File

@ -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 { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper'; import { Web3Wrapper } from '@0x/web3-wrapper';
import * as _ from 'lodash'; import * as _ from 'lodash';
@ -6,12 +7,10 @@ import * as React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
import { zrxDecimals } from '../constants';
import { Action, actions } from '../redux/actions'; import { Action, actions } from '../redux/actions';
import { State } from '../redux/reducer'; import { State } from '../redux/reducer';
import { ColorOption } from '../style/theme'; import { ColorOption } from '../style/theme';
import { AsyncProcessState } from '../types'; import { AsyncProcessState, ERC20Asset } from '../types';
import { assetBuyer } from '../util/asset_buyer';
import { errorUtil } from '../util/error'; import { errorUtil } from '../util/error';
import { AssetAmountInput } from '../components/asset_amount_input'; import { AssetAmountInput } from '../components/asset_amount_input';
@ -22,33 +21,52 @@ export interface SelectedAssetAmountInputProps {
} }
interface ConnectedState { interface ConnectedState {
assetBuyer?: AssetBuyer;
value?: BigNumber; value?: BigNumber;
assetData?: string; asset?: ERC20Asset;
} }
interface ConnectedDispatch { interface ConnectedDispatch {
onChange: (value?: BigNumber, assetData?: string) => void; updateBuyQuote: (assetBuyer?: AssetBuyer, value?: BigNumber, asset?: ERC20Asset) => void;
} }
const mapStateToProps = (state: State, _ownProps: SelectedAssetAmountInputProps): ConnectedState => ({ interface ConnectedProps {
value: state.selectedAssetAmount, value?: BigNumber;
assetData: state.selectedAssetData, 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 ( const updateBuyQuoteAsync = async (
assetBuyer: AssetBuyer,
dispatch: Dispatch<Action>, dispatch: Dispatch<Action>,
assetData: string, asset: ERC20Asset,
assetAmount: BigNumber, assetAmount: BigNumber,
): Promise<void> => { ): Promise<void> => {
// get a new buy quote. // get a new buy quote.
const baseUnitValue = Web3Wrapper.toBaseUnitAmount(assetAmount, zrxDecimals); const baseUnitValue = Web3Wrapper.toBaseUnitAmount(assetAmount, asset.metaData.decimals);
// mark quote as pending // mark quote as pending
dispatch(actions.setQuoteRequestStatePending()); dispatch(actions.setQuoteRequestStatePending());
let newBuyQuote: BuyQuote | undefined; let newBuyQuote: BuyQuote | undefined;
try { try {
newBuyQuote = await assetBuyer.getBuyQuoteAsync(assetData, baseUnitValue); newBuyQuote = await assetBuyer.getBuyQuoteAsync(asset.assetData, baseUnitValue);
} catch (error) { } catch (error) {
dispatch(actions.setQuoteRequestStateFailure()); dispatch(actions.setQuoteRequestStateFailure());
errorUtil.errorFlasher.flashNewError(dispatch, error); errorUtil.errorFlasher.flashNewError(dispatch, error);
@ -66,7 +84,7 @@ const mapDispatchToProps = (
dispatch: Dispatch<Action>, dispatch: Dispatch<Action>,
_ownProps: SelectedAssetAmountInputProps, _ownProps: SelectedAssetAmountInputProps,
): ConnectedDispatch => ({ ): ConnectedDispatch => ({
onChange: (value, assetData) => { updateBuyQuote: (assetBuyer, value, asset) => {
// Update the input // Update the input
dispatch(actions.updateSelectedAssetAmount(value)); dispatch(actions.updateSelectedAssetAmount(value));
// invalidate the last buy quote. // invalidate the last buy quote.
@ -74,16 +92,32 @@ const mapDispatchToProps = (
// reset our buy state // reset our buy state
dispatch(actions.updateBuyOrderState(AsyncProcessState.NONE)); 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 // even if it's debounced, give them the illusion it's loading
dispatch(actions.setQuoteRequestStatePending()); dispatch(actions.setQuoteRequestStatePending());
// tslint:disable-next-line:no-floating-promises // 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( export const SelectedAssetAmountInput: React.ComponentClass<SelectedAssetAmountInputProps> = connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps, mapDispatchToProps,
mergeProps,
)(AssetAmountInput); )(AssetAmountInput);

View File

@ -1,4 +1,4 @@
import { BuyQuote } from '@0x/asset-buyer'; import { AssetBuyer, BuyQuote } from '@0x/asset-buyer';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as React from 'react'; import * as React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -13,6 +13,7 @@ import { BuyButton } from '../components/buy_button';
export interface SelectedAssetBuyButtonProps {} export interface SelectedAssetBuyButtonProps {}
interface ConnectedState { interface ConnectedState {
assetBuyer?: AssetBuyer;
text: string; text: string;
buyQuote?: BuyQuote; buyQuote?: BuyQuote;
} }
@ -39,6 +40,7 @@ const textForState = (state: AsyncProcessState): string => {
}; };
const mapStateToProps = (state: State, _ownProps: SelectedAssetBuyButtonProps): ConnectedState => ({ const mapStateToProps = (state: State, _ownProps: SelectedAssetBuyButtonProps): ConnectedState => ({
assetBuyer: state.assetBuyer,
text: textForState(state.buyOrderState), text: textForState(state.buyOrderState),
buyQuote: state.latestBuyQuote, buyQuote: state.latestBuyQuote,
}); });

View File

@ -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);

View File

@ -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',
},
];

View File

@ -1,12 +1,11 @@
import { AssetProxyId, ObjectMap } from '@0x/types'; import { AssetProxyId, ObjectMap } from '@0x/types';
import { zrxAssetData } from '../constants';
import { AssetMetaData } from '../types'; import { AssetMetaData } from '../types';
// Map from assetData string to AssetMetaData object // Map from assetData string to AssetMetaData object
// TODO: import this from somewhere else. // TODO: import this from somewhere else.
export const assetMetaData: ObjectMap<AssetMetaData> = { export const assetMetaDataMap: ObjectMap<AssetMetaData> = {
[zrxAssetData]: { '0xf47261b0000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498': {
assetProxyId: AssetProxyId.ERC20, assetProxyId: AssetProxyId.ERC20,
decimals: 18, decimals: 18,
primaryColor: 'rgb(54, 50, 60)', primaryColor: 'rgb(54, 50, 60)',

View File

@ -1,10 +1,9 @@
import * as React from 'react'; import * as React from 'react';
import * as ReactDOM from 'react-dom'; 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: ZeroExInstantProps, selector: string = DEFAULT_ZERO_EX_CONTAINER_SELECTOR) => {
export const render = (props: ZeroExInstantOptions, selector: string = '#zeroExInstantContainer') => {
ReactDOM.render(React.createElement(ZeroExInstant, props), document.querySelector(selector)); ReactDOM.render(React.createElement(ZeroExInstant, props), document.querySelector(selector));
}; };

View File

@ -25,6 +25,7 @@ export enum ActionTypes {
UPDATE_SELECTED_ASSET_AMOUNT = 'UPDATE_SELECTED_ASSET_AMOUNT', UPDATE_SELECTED_ASSET_AMOUNT = 'UPDATE_SELECTED_ASSET_AMOUNT',
UPDATE_SELECTED_ASSET_BUY_STATE = 'UPDATE_SELECTED_ASSET_BUY_STATE', UPDATE_SELECTED_ASSET_BUY_STATE = 'UPDATE_SELECTED_ASSET_BUY_STATE',
UPDATE_LATEST_BUY_QUOTE = 'UPDATE_LATEST_BUY_QUOTE', 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_PENDING = 'SET_QUOTE_REQUEST_STATE_PENDING',
SET_QUOTE_REQUEST_STATE_FAILURE = 'SET_QUOTE_REQUEST_STATE_FAILURE', SET_QUOTE_REQUEST_STATE_FAILURE = 'SET_QUOTE_REQUEST_STATE_FAILURE',
SET_ERROR = 'SET_ERROR', SET_ERROR = 'SET_ERROR',
@ -38,6 +39,7 @@ export const actions = {
updateBuyOrderState: (buyState: AsyncProcessState) => updateBuyOrderState: (buyState: AsyncProcessState) =>
createAction(ActionTypes.UPDATE_SELECTED_ASSET_BUY_STATE, buyState), createAction(ActionTypes.UPDATE_SELECTED_ASSET_BUY_STATE, buyState),
updateLatestBuyQuote: (buyQuote?: BuyQuote) => createAction(ActionTypes.UPDATE_LATEST_BUY_QUOTE, buyQuote), 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), setQuoteRequestStatePending: () => createAction(ActionTypes.SET_QUOTE_REQUEST_STATE_PENDING),
setQuoteRequestStateFailure: () => createAction(ActionTypes.SET_QUOTE_REQUEST_STATE_FAILURE), setQuoteRequestStateFailure: () => createAction(ActionTypes.SET_QUOTE_REQUEST_STATE_FAILURE),
setError: (error?: any) => createAction(ActionTypes.SET_ERROR, error), setError: (error?: any) => createAction(ActionTypes.SET_ERROR, error),

View File

@ -3,10 +3,10 @@ import { coinbaseApi } from '../util/coinbase_api';
import { ActionTypes } from './actions'; import { ActionTypes } from './actions';
import { store } from './store'; import { Store } from './store';
export const asyncData = { export const asyncData = {
fetchAndDispatchToStore: async () => { fetchAndDispatchToStore: async (store: Store) => {
let ethUsdPrice = BIG_NUMBER_ZERO; let ethUsdPrice = BIG_NUMBER_ZERO;
try { try {
ethUsdPrice = await coinbaseApi.getEthUsdPrice(); ethUsdPrice = await coinbaseApi.getEthUsdPrice();

View File

@ -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 { BigNumber } from '@0x/utils';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { zrxAssetData } from '../constants'; import { assetMetaDataMap } from '../data/asset_meta_data_map';
import { AsyncProcessState, DisplayStatus } from '../types'; import { Asset, AssetMetaData, AsyncProcessState, DisplayStatus, Network } from '../types';
import { assetUtils } from '../util/asset';
import { Action, ActionTypes } from './actions'; import { Action, ActionTypes } from './actions';
export interface State { export interface State {
selectedAssetData?: string; network: Network;
assetBuyer?: AssetBuyer;
assetMetaDataMap: ObjectMap<AssetMetaData>;
selectedAsset?: Asset;
selectedAssetAmount?: BigNumber; selectedAssetAmount?: BigNumber;
buyOrderState: AsyncProcessState; buyOrderState: AsyncProcessState;
ethUsdPrice?: BigNumber; ethUsdPrice?: BigNumber;
@ -19,9 +24,9 @@ export interface State {
} }
export const INITIAL_STATE: State = { export const INITIAL_STATE: State = {
// TODO: Remove hardcoded zrxAssetData network: Network.Mainnet,
selectedAssetData: zrxAssetData,
selectedAssetAmount: undefined, selectedAssetAmount: undefined,
assetMetaDataMap,
buyOrderState: AsyncProcessState.NONE, buyOrderState: AsyncProcessState.NONE,
ethUsdPrice: undefined, ethUsdPrice: undefined,
latestBuyQuote: undefined, latestBuyQuote: undefined,
@ -82,6 +87,20 @@ export const reducer = (state: State = INITIAL_STATE, action: Action): State =>
latestError: undefined, latestError: undefined,
latestErrorDisplay: DisplayStatus.Hidden, 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: default:
return state; return state;
} }

View File

@ -4,4 +4,10 @@ import { devToolsEnhancer } from 'redux-devtools-extension/developmentOnly';
import { reducer, State } from './reducer'; 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({}));
},
};

View File

@ -26,12 +26,32 @@ export interface ERC20AssetMetaData {
export interface ERC721AssetMetaData { export interface ERC721AssetMetaData {
assetProxyId: AssetProxyId.ERC721; assetProxyId: AssetProxyId.ERC721;
name: string; name: string;
representationUrl?: string;
primaryColor?: string; primaryColor?: string;
} }
export type AssetMetaData = ERC20AssetMetaData | ERC721AssetMetaData; 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 { export enum Network {
Kovan = 42, Kovan = 42,
Mainnet = 1, Mainnet = 1,
} }
export enum ZeroExInstantError {
AssetMetaDataNotAvailable = 'ASSET_META_DATA_NOT_AVAILABLE',
}

View File

@ -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];
},
};

View File

@ -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);

View File

@ -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;
},
};

View File

@ -2,7 +2,9 @@ import { AssetBuyerError } from '@0x/asset-buyer';
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
import { Action, actions } from '../redux/actions'; import { Action, actions } from '../redux/actions';
import { assetDataUtil } from '../util/asset_data'; import { Asset } from '../types';
import { assetUtils } from './asset';
class ErrorFlasher { class ErrorFlasher {
private _timeoutId?: number; 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 = const hasInsufficientLiquidity =
error.message === AssetBuyerError.InsufficientAssetLiquidity || error.message === AssetBuyerError.InsufficientAssetLiquidity ||
error.message === AssetBuyerError.InsufficientZrxLiquidity; error.message === AssetBuyerError.InsufficientZrxLiquidity;
if (hasInsufficientLiquidity) { if (hasInsufficientLiquidity) {
const assetName = assetDataUtil.bestNameForAsset(assetData, 'of this asset'); const assetName = assetUtils.bestNameForAsset(asset, 'of this asset');
return `Not enough ${assetName} available`; return `Not enough ${assetName} available`;
} }
@ -40,7 +42,7 @@ const humanReadableMessageForError = (error: Error, assetData?: string): string
error.message === AssetBuyerError.StandardRelayerApiError || error.message === AssetBuyerError.StandardRelayerApiError ||
error.message.startsWith(AssetBuyerError.AssetUnavailable) error.message.startsWith(AssetBuyerError.AssetUnavailable)
) { ) {
const assetName = assetDataUtil.bestNameForAsset(assetData, 'This asset'); const assetName = assetUtils.bestNameForAsset(asset, 'This asset');
return `${assetName} is currently unavailable`; return `${assetName} is currently unavailable`;
} }
@ -49,10 +51,10 @@ const humanReadableMessageForError = (error: Error, assetData?: string): string
export const errorUtil = { export const errorUtil = {
errorFlasher: new ErrorFlasher(), 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; let bestMessage: string | undefined;
if (error instanceof Error) { if (error instanceof Error) {
bestMessage = humanReadableMessageForError(error, assetData); bestMessage = humanReadableMessageForError(error, asset);
} }
return { return {
icon: '😢', icon: '😢',

View File

@ -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);
});
});
});

View File

@ -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');
});
});
});

View File

@ -1,14 +1,24 @@
import { AssetBuyerError } from '@0x/asset-buyer'; import { AssetBuyerError } from '@0x/asset-buyer';
import { AssetProxyId } from '@0x/types';
import { Asset } from '../../src/types';
import { errorUtil } from '../../src/util/error'; import { errorUtil } from '../../src/util/error';
const ZRX_ASSET_DATA = '0xf47261b0000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498'; const ZRX_ASSET_DATA = '0xf47261b0000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498';
const ZRX_ASSET: Asset = {
assetData: ZRX_ASSET_DATA,
metaData: {
assetProxyId: AssetProxyId.ERC20,
symbol: 'zrx',
decimals: 18,
},
};
describe('errorUtil', () => { describe('errorUtil', () => {
describe('errorFlasher', () => { describe('errorFlasher', () => {
it('should return error and asset name for InsufficientAssetLiquidity', () => { it('should return error and asset name for InsufficientAssetLiquidity', () => {
const insufficientAssetError = new Error(AssetBuyerError.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', 'Not enough ZRX available',
); );
}); });
@ -20,27 +30,25 @@ describe('errorUtil', () => {
}); });
it('should return asset name for InsufficientAssetLiquidity', () => { it('should return asset name for InsufficientAssetLiquidity', () => {
const insufficientZrxError = new Error(AssetBuyerError.InsufficientZrxLiquidity); 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', 'Not enough ZRX available',
); );
}); });
it('should return unavailable error and asset name for StandardRelayerApiError', () => { it('should return unavailable error and asset name for StandardRelayerApiError', () => {
const standardRelayerError = new Error(AssetBuyerError.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', 'ZRX is currently unavailable',
); );
}); });
it('should return error for AssetUnavailable error', () => { it('should return error for AssetUnavailable error', () => {
const assetUnavailableError = new Error( const assetUnavailableError = new Error(`${AssetBuyerError.AssetUnavailable}: For assetData ${ZRX_ASSET}`);
`${AssetBuyerError.AssetUnavailable}: For assetData ${ZRX_ASSET_DATA}`, expect(errorUtil.errorDescription(assetUnavailableError, ZRX_ASSET).message).toEqual(
);
expect(errorUtil.errorDescription(assetUnavailableError, ZRX_ASSET_DATA).message).toEqual(
'ZRX is currently unavailable', 'ZRX is currently unavailable',
); );
}); });
it('should return default for AssetUnavailable error', () => { it('should return default for AssetUnavailable error', () => {
const assetUnavailableError = new Error(`${AssetBuyerError.AssetUnavailable}: For assetData xyz`); 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', 'This asset is currently unavailable',
); );
}); });

View File

@ -1871,7 +1871,7 @@ aes-js@^0.2.3:
aes-js@^3.1.1: aes-js@^3.1.1:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.1.1.tgz#89fd1f94ae51b4c72d62466adc1a7323ff52f072" resolved "https://registry.npmjs.org/aes-js/-/aes-js-3.1.1.tgz#89fd1f94ae51b4c72d62466adc1a7323ff52f072"
agent-base@4, agent-base@^4.1.0, agent-base@~4.2.0: agent-base@4, agent-base@^4.1.0, agent-base@~4.2.0:
version "4.2.1" version "4.2.1"
@ -3328,7 +3328,7 @@ bs58check@^1.0.8:
bs58check@^2.1.2: bs58check@^2.1.2:
version "2.1.2" version "2.1.2"
resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-2.1.2.tgz#53b018291228d82a5aa08e7d796fdafda54aebfc" resolved "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz#53b018291228d82a5aa08e7d796fdafda54aebfc"
dependencies: dependencies:
bs58 "^4.0.0" bs58 "^4.0.0"
create-hash "^1.1.0" create-hash "^1.1.0"
@ -5915,7 +5915,7 @@ ethereumjs-wallet@0.6.0:
ethereumjs-wallet@~0.6.0: ethereumjs-wallet@~0.6.0:
version "0.6.2" version "0.6.2"
resolved "https://registry.yarnpkg.com/ethereumjs-wallet/-/ethereumjs-wallet-0.6.2.tgz#67244b6af3e8113b53d709124b25477b64aeccda" resolved "https://registry.npmjs.org/ethereumjs-wallet/-/ethereumjs-wallet-0.6.2.tgz#67244b6af3e8113b53d709124b25477b64aeccda"
dependencies: dependencies:
aes-js "^3.1.1" aes-js "^3.1.1"
bs58check "^2.1.2" bs58check "^2.1.2"
@ -6730,7 +6730,7 @@ ganache-core@0xProject/ganache-core#monorepo-dep:
ethereumjs-tx "0xProject/ethereumjs-tx#fake-tx-include-signature-by-default" ethereumjs-tx "0xProject/ethereumjs-tx#fake-tx-include-signature-by-default"
ethereumjs-util "^5.2.0" ethereumjs-util "^5.2.0"
ethereumjs-vm "2.3.5" ethereumjs-vm "2.3.5"
ethereumjs-wallet "0.6.0" ethereumjs-wallet "~0.6.0"
fake-merkle-patricia-tree "~1.0.1" fake-merkle-patricia-tree "~1.0.1"
heap "~0.2.6" heap "~0.2.6"
js-scrypt "^0.2.0" js-scrypt "^0.2.0"
@ -7457,7 +7457,7 @@ hdkey@^0.7.0, hdkey@^0.7.1:
hdkey@^1.0.0: hdkey@^1.0.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/hdkey/-/hdkey-1.1.0.tgz#e74e7b01d2c47f797fa65d1d839adb7a44639f29" resolved "https://registry.npmjs.org/hdkey/-/hdkey-1.1.0.tgz#e74e7b01d2c47f797fa65d1d839adb7a44639f29"
dependencies: dependencies:
coinstring "^2.0.0" coinstring "^2.0.0"
safe-buffer "^5.1.1" safe-buffer "^5.1.1"
@ -15548,7 +15548,7 @@ utf8@^2.1.1:
utf8@^3.0.0: utf8@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/utf8/-/utf8-3.0.0.tgz#f052eed1364d696e769ef058b183df88c87f69d1" resolved "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz#f052eed1364d696e769ef058b183df88c87f69d1"
util-deprecate@~1.0.1: util-deprecate@~1.0.1:
version "1.0.2" version "1.0.2"