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:
commit
4a72dc6c6f
@ -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",
|
||||
|
@ -25,7 +25,8 @@
|
||||
<div id="zeroExInstantContainer"></div>
|
||||
<script>
|
||||
zeroExInstant.render({
|
||||
|
||||
liquiditySource: 'https://api.radarrelay.com/0x/v2/',
|
||||
assetData: '0xf47261b0000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498',
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
|
||||
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();
|
||||
asyncData.fetchAndDispatchToStore(this._store);
|
||||
}
|
||||
|
||||
export interface ZeroExInstantProps {}
|
||||
|
||||
export const ZeroExInstant: React.StatelessComponent<ZeroExInstantProps> = () => (
|
||||
<Provider store={store}>
|
||||
<ThemeProvider theme={theme}>
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<Provider store={this._store}>
|
||||
<SelectedAssetThemeProvider>
|
||||
<ZeroExInstantContainer />
|
||||
</ThemeProvider>
|
||||
</SelectedAssetThemeProvider>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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',
|
||||
});
|
||||
|
@ -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 => ({
|
||||
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,
|
||||
assetData: state.selectedAssetData,
|
||||
});
|
||||
};
|
||||
}
|
||||
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);
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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);
|
15
packages/instant/src/data/asset_data_network_mapping.ts
Normal file
15
packages/instant/src/data/asset_data_network_mapping.ts
Normal 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',
|
||||
},
|
||||
];
|
@ -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)',
|
@ -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));
|
||||
};
|
||||
|
@ -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),
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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({}));
|
||||
},
|
||||
};
|
||||
|
@ -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',
|
||||
}
|
||||
|
53
packages/instant/src/util/asset.ts
Normal file
53
packages/instant/src/util/asset.ts
Normal 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];
|
||||
},
|
||||
};
|
@ -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);
|
@ -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;
|
||||
},
|
||||
};
|
@ -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: '😢',
|
||||
|
47
packages/instant/test/util/asset.test.ts
Normal file
47
packages/instant/test/util/asset.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
@ -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',
|
||||
);
|
||||
});
|
||||
|
12
yarn.lock
12
yarn.lock
@ -1871,7 +1871,7 @@ aes-js@^0.2.3:
|
||||
|
||||
aes-js@^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:
|
||||
version "4.2.1"
|
||||
@ -3328,7 +3328,7 @@ bs58check@^1.0.8:
|
||||
|
||||
bs58check@^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:
|
||||
bs58 "^4.0.0"
|
||||
create-hash "^1.1.0"
|
||||
@ -5915,7 +5915,7 @@ ethereumjs-wallet@0.6.0:
|
||||
|
||||
ethereumjs-wallet@~0.6.0:
|
||||
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:
|
||||
aes-js "^3.1.1"
|
||||
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-util "^5.2.0"
|
||||
ethereumjs-vm "2.3.5"
|
||||
ethereumjs-wallet "0.6.0"
|
||||
ethereumjs-wallet "~0.6.0"
|
||||
fake-merkle-patricia-tree "~1.0.1"
|
||||
heap "~0.2.6"
|
||||
js-scrypt "^0.2.0"
|
||||
@ -7457,7 +7457,7 @@ hdkey@^0.7.0, hdkey@^0.7.1:
|
||||
|
||||
hdkey@^1.0.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:
|
||||
coinstring "^2.0.0"
|
||||
safe-buffer "^5.1.1"
|
||||
@ -15548,7 +15548,7 @@ utf8@^2.1.1:
|
||||
|
||||
utf8@^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:
|
||||
version "1.0.2"
|
||||
|
Loading…
x
Reference in New Issue
Block a user