diff options
23 files changed, 263 insertions, 241 deletions
diff --git a/packages/asset-buyer/CHANGELOG.json b/packages/asset-buyer/CHANGELOG.json index 7ebcd8c2f..5d6604ea9 100644 --- a/packages/asset-buyer/CHANGELOG.json +++ b/packages/asset-buyer/CHANGELOG.json @@ -1,5 +1,14 @@ [ { + "version": "2.2.0", + "changes": [ + { + "note": "`getAssetBuyerForProvidedOrders` factory function now takes 3 args instead of 4", + "pr": 1187 + } + ] + }, + { "version": "2.1.0", "changes": [ { diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts index 74f3cb471..34e2d9639 100644 --- a/packages/asset-buyer/src/asset_buyer.ts +++ b/packages/asset-buyer/src/asset_buyer.ts @@ -52,16 +52,13 @@ export class AssetBuyer { public static getAssetBuyerForProvidedOrders( provider: Provider, orders: SignedOrder[], - feeOrders: SignedOrder[] = [], options: Partial<AssetBuyerOpts> = {}, ): AssetBuyer { assert.isWeb3Provider('provider', provider); assert.doesConformToSchema('orders', orders, schemas.signedOrdersSchema); - assert.doesConformToSchema('feeOrders', feeOrders, schemas.signedOrdersSchema); assert.areValidProvidedOrders('orders', orders); - assert.areValidProvidedOrders('feeOrders', feeOrders); assert.assert(orders.length !== 0, `Expected orders to contain at least one order`); - const orderProvider = new BasicOrderProvider(_.concat(orders, feeOrders)); + const orderProvider = new BasicOrderProvider(orders); const assetBuyer = new AssetBuyer(provider, orderProvider, options); return assetBuyer; } diff --git a/packages/instant/package.json b/packages/instant/package.json index 0329c3078..81d2e4c7b 100644 --- a/packages/instant/package.json +++ b/packages/instant/package.json @@ -44,7 +44,9 @@ }, "homepage": "https://github.com/0xProject/0x-monorepo/packages/instant/README.md", "dependencies": { + "@0x/assert": "^1.0.14", "@0x/asset-buyer": "^2.1.0", + "@0x/json-schemas": "^2.0.0", "@0x/order-utils": "^2.0.0", "@0x/types": "^1.2.0", "@0x/typescript-typings": "^3.0.3", diff --git a/packages/instant/public/index.html b/packages/instant/public/index.html index 14555fc64..9f1dfdb64 100644 --- a/packages/instant/public/index.html +++ b/packages/instant/public/index.html @@ -6,6 +6,8 @@ <meta name="viewport" content="width=device-width, initial-scale=1"> <title>0x Instant Dev Environment</title> <script type="text/javascript" src="/main.bundle.js" charset="utf-8"></script> + <script type="text/javascript" src="https://unpkg.com/jsuri@1.3.1/Uri.js" charset="utf-8"></script> + <script type="text/javascript" src="https://unpkg.com/bignumber.js@4.1.0/bignumber.js" charset="utf-8"></script> <style> #zeroExInstantContainer { display: flex; @@ -24,10 +26,47 @@ <body> <div id="zeroExInstantContainer"></div> <script> - zeroExInstant.render({ + const removeUndefined = (obj) => { + for (let k in obj) if (obj[k] === undefined) delete obj[k]; + return obj; + } + BigNumber.config({ + EXPONENTIAL_AT: 1000, + DECIMAL_PLACES: 78, + }); + const providedOrder = { + senderAddress: '0x0000000000000000000000000000000000000000', + makerAddress: '0x14e2f1f157e7dd4057d02817436d628a37120fd1', + takerAddress: '0x0000000000000000000000000000000000000000', + makerFee: new BigNumber('0'), + takerFee: new BigNumber('0'), + makerAssetAmount: new BigNumber('100000000000000000000'), + takerAssetAmount: new BigNumber('10000000000000000'), + makerAssetData: '0xf47261b00000000000000000000000002002d3812f58e35f0ea1ffbf80a75a38c32175fa', + takerAssetData: '0xf47261b0000000000000000000000000d0a1e359811322d97991e03f863a0c30c2cf029c', + expirationTimeSeconds: new BigNumber('1591858800'), + feeRecipientAddress: '0x0000000000000000000000000000000000000000', + salt: new BigNumber( + '54983920541892966634674340965984367456810207583416050222519063020710969340046', + ), + signature: + '0x1b949656218421c845995457303569a656764afa2b979d41dcefff0009d57ce15001490268bc7caa4269894fd83b741465fc5a7a53eda6ece17eb91fb32655d83703', + exchangeAddress: '0x35dd2932454449b14cee11a94d3674a936d5d7b2', + }; + const queryParams = new Uri(window.location.search); + const renderOptionsDefaults = { liquiditySource: 'https://api.radarrelay.com/0x/v2/', assetData: '0xf47261b0000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498', - }); + } + const liquiditySourceOverride = queryParams.getQueryParamValue('liquiditySource'); + const renderOptionsOverrides = { + liquiditySource: liquiditySourceOverride === 'provided' ? [providedOrder] : liquiditySourceOverride, + assetData: queryParams.getQueryParamValue('assetData'), + networkId: +queryParams.getQueryParamValue('networkId') || undefined, + defaultAssetBuyAmount: +queryParams.getQueryParamValue('defaultAssetBuyAmount') || undefined, + } + const renderOptions = Object.assign({}, renderOptionsDefaults, removeUndefined(renderOptionsOverrides)); + zeroExInstant.render(renderOptions); </script> </body> diff --git a/packages/instant/src/components/buy_button.tsx b/packages/instant/src/components/buy_button.tsx index bcd435250..129aedaf3 100644 --- a/packages/instant/src/components/buy_button.tsx +++ b/packages/instant/src/components/buy_button.tsx @@ -17,7 +17,7 @@ export interface BuyButtonProps { assetBuyer?: AssetBuyer; onValidationPending: (buyQuote: BuyQuote) => void; onValidationFail: (buyQuote: BuyQuote, errorMessage: AssetBuyerError | ZeroExInstantError) => void; - onSignatureDenied: (buyQuote: BuyQuote, preventedError: Error) => void; + onSignatureDenied: (buyQuote: BuyQuote) => void; onBuyProcessing: (buyQuote: BuyQuote, txHash: string) => void; onBuySuccess: (buyQuote: BuyQuote, txHash: string) => void; onBuyFailure: (buyQuote: BuyQuote, txHash: string) => void; @@ -49,8 +49,8 @@ export class BuyButton extends React.Component<BuyButtonProps> { this.props.onValidationPending(buyQuote); const takerAddress = await getBestAddress(); - const hasSufficentEth = await balanceUtil.hasSufficentEth(takerAddress, buyQuote, web3Wrapper); - if (!hasSufficentEth) { + const hasSufficientEth = await balanceUtil.hasSufficientEth(takerAddress, buyQuote, web3Wrapper); + if (!hasSufficientEth) { this.props.onValidationFail(buyQuote, ZeroExInstantError.InsufficientETH); return; } @@ -61,7 +61,7 @@ export class BuyButton extends React.Component<BuyButtonProps> { } catch (e) { if (e instanceof Error) { if (e.message === AssetBuyerError.SignatureRequestDenied) { - this.props.onSignatureDenied(buyQuote, e); + this.props.onSignatureDenied(buyQuote); return; } else if (e.message === AssetBuyerError.TransactionValueTooLow) { this.props.onValidationFail(buyQuote, AssetBuyerError.TransactionValueTooLow); diff --git a/packages/instant/src/components/buy_order_state_buttons.tsx b/packages/instant/src/components/buy_order_state_buttons.tsx index 7c06ff31b..d01e9ff57 100644 --- a/packages/instant/src/components/buy_order_state_buttons.tsx +++ b/packages/instant/src/components/buy_order_state_buttons.tsx @@ -19,7 +19,7 @@ export interface BuyOrderStateButtonProps { onViewTransaction: () => void; onValidationPending: (buyQuote: BuyQuote) => void; onValidationFail: (buyQuote: BuyQuote, errorMessage: AssetBuyerError | ZeroExInstantError) => void; - onSignatureDenied: (buyQuote: BuyQuote, error: Error) => void; + onSignatureDenied: (buyQuote: BuyQuote) => void; onBuyProcessing: (buyQuote: BuyQuote, txHash: string) => void; onBuySuccess: (buyQuote: BuyQuote, txHash: string) => void; onBuyFailure: (buyQuote: BuyQuote, txHash: string) => void; diff --git a/packages/instant/src/components/sliding_error.tsx b/packages/instant/src/components/sliding_error.tsx index 3865a8797..cc9abb7dd 100644 --- a/packages/instant/src/components/sliding_error.tsx +++ b/packages/instant/src/components/sliding_error.tsx @@ -4,7 +4,7 @@ import { ColorOption } from '../style/theme'; import { SlideDownAnimation, SlideUpAnimation } from './animations/slide_animations'; -import { Container, Text } from './ui'; +import { Container, Flex, Text } from './ui'; export interface ErrorProps { icon: string; @@ -20,12 +20,14 @@ export const Error: React.StatelessComponent<ErrorProps> = props => ( borderRadius="6px" marginBottom="10px" > - <Container marginRight="5px" display="inline" top="3px" position="relative"> - <Text fontSize="20px">{props.icon}</Text> - </Container> - <Text fontWeight="500" fontColor={ColorOption.darkOrange}> - {props.message} - </Text> + <Flex justify="flex-start"> + <Container marginRight="5px" display="inline" top="3px" position="relative"> + <Text fontSize="20px">{props.icon}</Text> + </Container> + <Text fontWeight="500" fontColor={ColorOption.darkOrange}> + {props.message} + </Text> + </Flex> </Container> ); diff --git a/packages/instant/src/components/zero_ex_instant.tsx b/packages/instant/src/components/zero_ex_instant.tsx index ffa5a8250..19a2d6b9b 100644 --- a/packages/instant/src/components/zero_ex_instant.tsx +++ b/packages/instant/src/components/zero_ex_instant.tsx @@ -1,5 +1,6 @@ import { AssetBuyer } from '@0x/asset-buyer'; -import { ObjectMap } from '@0x/types'; +import { ObjectMap, SignedOrder } from '@0x/types'; +import * as _ from 'lodash'; import * as React from 'react'; import { Provider } from 'react-redux'; @@ -10,7 +11,10 @@ import { store, Store } from '../redux/store'; import { fonts } from '../style/fonts'; import { AssetMetaData, Network } from '../types'; import { assetUtils } from '../util/asset'; +import { BigNumberInput } from '../util/big_number_input'; +import { errorFlasher } from '../util/error_flasher'; import { getProvider } from '../util/provider'; +import { web3Wrapper } from '../util/web3_wrapper'; import { ZeroExInstantContainer } from './zero_ex_instant_container'; @@ -21,28 +25,34 @@ export type ZeroExInstantProps = ZeroExInstantRequiredProps & Partial<ZeroExInst 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; + liquiditySource: string | SignedOrder[]; } export interface ZeroExInstantOptionalProps { + defaultAssetBuyAmount?: number; additionalAssetMetaDataMap: ObjectMap<AssetMetaData>; - network: Network; + networkId: 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; + const networkId = props.networkId || state.network; // TODO: Provider needs to not be hard-coded to injected web3. - const assetBuyer = AssetBuyer.getAssetBuyerForStandardRelayerAPIUrl(getProvider(), props.liquiditySource, { - networkId: network, - }); + const provider = getProvider(); + const assetBuyerOptions = { + networkId, + }; + let assetBuyer; + if (_.isString(props.liquiditySource)) { + assetBuyer = AssetBuyer.getAssetBuyerForStandardRelayerAPIUrl( + provider, + props.liquiditySource, + assetBuyerOptions, + ); + } else { + assetBuyer = AssetBuyer.getAssetBuyerForProvidedOrders(provider, props.liquiditySource, assetBuyerOptions); + } const completeAssetMetaDataMap = { ...props.additionalAssetMetaDataMap, ...state.assetMetaDataMap, @@ -50,17 +60,26 @@ export class ZeroExInstant extends React.Component<ZeroExInstantProps> { const storeStateFromProps: State = { ...state, assetBuyer, - network, - selectedAsset: assetUtils.createAssetFromAssetData(props.assetData, completeAssetMetaDataMap, network), + network: networkId, + selectedAsset: assetUtils.createAssetFromAssetData(props.assetData, completeAssetMetaDataMap, networkId), + selectedAssetAmount: _.isUndefined(props.defaultAssetBuyAmount) + ? state.selectedAssetAmount + : new BigNumberInput(props.defaultAssetBuyAmount), assetMetaDataMap: completeAssetMetaDataMap, }; return storeStateFromProps; } constructor(props: ZeroExInstantProps) { super(props); - this._store = store.create(ZeroExInstant._mergeInitialStateWithProps(this.props, INITIAL_STATE)); + const initialAppState = ZeroExInstant._mergeInitialStateWithProps(this.props, INITIAL_STATE); + this._store = store.create(initialAppState); + } + + public componentDidMount(): void { // tslint:disable-next-line:no-floating-promises asyncData.fetchAndDispatchToStore(this._store); + // tslint:disable-next-line:no-floating-promises + this._flashErrorIfWrongNetwork(); } public render(): React.ReactNode { @@ -72,4 +91,14 @@ export class ZeroExInstant extends React.Component<ZeroExInstantProps> { </Provider> ); } + + private readonly _flashErrorIfWrongNetwork = async (): Promise<void> => { + const msToShowError = 30000; // 30 seconds + const network = this._store.getState().network; + const networkOfProvider = await web3Wrapper.getNetworkIdAsync(); + if (network !== networkOfProvider) { + const errorMessage = `Wrong network detected. Try switching to ${Network[network]}.`; + errorFlasher.flashNewErrorMessage(this._store.dispatch, errorMessage, msToShowError); + } + }; } diff --git a/packages/instant/src/containers/latest_error.tsx b/packages/instant/src/containers/latest_error.tsx index b75ec00aa..45ca09673 100644 --- a/packages/instant/src/containers/latest_error.tsx +++ b/packages/instant/src/containers/latest_error.tsx @@ -5,32 +5,30 @@ import { connect } from 'react-redux'; import { SlidingError } from '../components/sliding_error'; import { State } from '../redux/reducer'; import { Asset, DisplayStatus } from '../types'; -import { errorUtil } from '../util/error'; export interface LatestErrorComponentProps { asset?: Asset; - latestError?: any; + latestErrorMessage?: string; slidingDirection: 'down' | 'up'; } export const LatestErrorComponent: React.StatelessComponent<LatestErrorComponentProps> = props => { - if (!props.latestError) { + if (!props.latestErrorMessage) { return <div />; } - const { icon, message } = errorUtil.errorDescription(props.latestError, props.asset); - return <SlidingError direction={props.slidingDirection} icon={icon} message={message} />; + return <SlidingError direction={props.slidingDirection} icon="😢" message={props.latestErrorMessage} />; }; interface ConnectedState { asset?: Asset; - latestError?: any; + latestErrorMessage?: string; slidingDirection: 'down' | 'up'; } export interface LatestErrorProps {} const mapStateToProps = (state: State, _ownProps: LatestErrorProps): ConnectedState => ({ asset: state.selectedAsset, - latestError: state.latestError, - slidingDirection: state.latestErrorDisplay === DisplayStatus.Present ? 'up' : 'down', + latestErrorMessage: state.latestErrorMessage, + slidingDirection: state.latestErrorDisplayStatus === DisplayStatus.Present ? 'up' : 'down', }); export const LatestError = connect(mapStateToProps)(LatestErrorComponent); diff --git a/packages/instant/src/containers/selected_asset_buy_order_state_buttons.ts b/packages/instant/src/containers/selected_asset_buy_order_state_buttons.ts index 241c0192c..500d6b88a 100644 --- a/packages/instant/src/containers/selected_asset_buy_order_state_buttons.ts +++ b/packages/instant/src/containers/selected_asset_buy_order_state_buttons.ts @@ -4,14 +4,13 @@ import * as React from 'react'; import { connect } from 'react-redux'; import { Dispatch } from 'redux'; +import { BuyOrderStateButtons } from '../components/buy_order_state_buttons'; import { Action, actions } from '../redux/actions'; import { State } from '../redux/reducer'; import { OrderProcessState, OrderState, ZeroExInstantError } from '../types'; +import { errorFlasher } from '../util/error_flasher'; import { etherscanUtil } from '../util/etherscan'; -import { BuyOrderStateButtons } from '../components/buy_order_state_buttons'; -import { errorUtil } from '../util/error'; - interface ConnectedState { buyQuote?: BuyQuote; buyOrderProcessingState: OrderProcessState; @@ -21,7 +20,7 @@ interface ConnectedState { interface ConnectedDispatch { onValidationPending: (buyQuote: BuyQuote) => void; - onSignatureDenied: (buyQuote: BuyQuote, error: Error) => void; + onSignatureDenied: (buyQuote: BuyQuote) => void; onBuyProcessing: (buyQuote: BuyQuote, txHash: string) => void; onBuySuccess: (buyQuote: BuyQuote, txHash: string) => void; onBuyFailure: (buyQuote: BuyQuote, txHash: string) => void; @@ -68,13 +67,19 @@ const mapDispatchToProps = ( dispatch(actions.updateBuyOrderState({ processState: OrderProcessState.SUCCESS, txHash })), onBuyFailure: (buyQuote: BuyQuote, txHash: string) => dispatch(actions.updateBuyOrderState({ processState: OrderProcessState.FAILURE, txHash })), - onSignatureDenied: (buyQuote, error) => { + onSignatureDenied: () => { dispatch(actions.resetAmount()); - errorUtil.errorFlasher.flashNewError(dispatch, error); + const errorMessage = 'You denied this transaction'; + errorFlasher.flashNewErrorMessage(dispatch, errorMessage); }, onValidationFail: (buyQuote, error) => { dispatch(actions.updateBuyOrderState({ processState: OrderProcessState.NONE })); - errorUtil.errorFlasher.flashNewError(dispatch, new Error(error)); + if (error === ZeroExInstantError.InsufficientETH) { + const errorMessage = "You don't have enough ETH"; + errorFlasher.flashNewErrorMessage(dispatch, errorMessage); + } else { + errorFlasher.flashNewErrorMessage(dispatch); + } }, onRetry: () => { dispatch(actions.resetAmount()); 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 41ed974a2..4767b15d4 100644 --- a/packages/instant/src/containers/selected_erc20_asset_amount_input.ts +++ b/packages/instant/src/containers/selected_erc20_asset_amount_input.ts @@ -1,4 +1,4 @@ -import { AssetBuyer, BuyQuote } from '@0x/asset-buyer'; +import { AssetBuyer, AssetBuyerError, BuyQuote } from '@0x/asset-buyer'; import { AssetProxyId } from '@0x/types'; import { BigNumber } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; @@ -12,8 +12,9 @@ import { Action, actions } from '../redux/actions'; import { State } from '../redux/reducer'; import { ColorOption } from '../style/theme'; import { ERC20Asset, OrderProcessState } from '../types'; +import { assetUtils } from '../util/asset'; import { BigNumberInput } from '../util/big_number_input'; -import { errorUtil } from '../util/error'; +import { errorFlasher } from '../util/error_flasher'; export interface SelectedERC20AssetAmountInputProps { fontColor?: ColorOption; @@ -78,11 +79,24 @@ const updateBuyQuoteAsync = async ( newBuyQuote = await assetBuyer.getBuyQuoteAsync(asset.assetData, baseUnitValue); } catch (error) { dispatch(actions.setQuoteRequestStateFailure()); - errorUtil.errorFlasher.flashNewError(dispatch, error); + let errorMessage; + if (error.message === AssetBuyerError.InsufficientAssetLiquidity) { + const assetName = assetUtils.bestNameForAsset(asset, 'of this asset'); + errorMessage = `Not enough ${assetName} available`; + } else if (error.message === AssetBuyerError.InsufficientZrxLiquidity) { + errorMessage = 'Not enough ZRX available'; + } else if ( + error.message === AssetBuyerError.StandardRelayerApiError || + error.message.startsWith(AssetBuyerError.AssetUnavailable) + ) { + const assetName = assetUtils.bestNameForAsset(asset, 'This asset'); + errorMessage = `${assetName} is currently unavailable`; + } + errorFlasher.flashNewErrorMessage(dispatch, errorMessage); return; } // We have a successful new buy quote - errorUtil.errorFlasher.clearError(dispatch); + errorFlasher.clearError(dispatch); // invalidate the last buy quote. dispatch(actions.updateLatestBuyQuote(newBuyQuote)); }; diff --git a/packages/instant/src/index.umd.ts b/packages/instant/src/index.umd.ts index f648b37f2..dabd45cae 100644 --- a/packages/instant/src/index.umd.ts +++ b/packages/instant/src/index.umd.ts @@ -1,9 +1,22 @@ +import * as _ from 'lodash'; import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { DEFAULT_ZERO_EX_CONTAINER_SELECTOR } from './constants'; import { ZeroExInstant, ZeroExInstantProps } from './index'; +import { assert } from './util/assert'; export const render = (props: ZeroExInstantProps, selector: string = DEFAULT_ZERO_EX_CONTAINER_SELECTOR) => { + assert.isHexString('assetData', props.assetData); + assert.isValidLiquiditySource('liquiditySource', props.liquiditySource); + if (!_.isUndefined(props.additionalAssetMetaDataMap)) { + assert.isValidAssetMetaDataMap('additionalAssetMetaDataMap', props.additionalAssetMetaDataMap); + } + if (!_.isUndefined(props.defaultAssetBuyAmount)) { + assert.isNumber('defaultAssetBuyAmount', props.defaultAssetBuyAmount); + } + if (!_.isUndefined(props.networkId)) { + assert.isNumber('networkId', props.networkId); + } ReactDOM.render(React.createElement(ZeroExInstant, props), document.querySelector(selector)); }; diff --git a/packages/instant/src/redux/actions.ts b/packages/instant/src/redux/actions.ts index 46045024b..bfae68e2b 100644 --- a/packages/instant/src/redux/actions.ts +++ b/packages/instant/src/redux/actions.ts @@ -30,7 +30,7 @@ export enum ActionTypes { 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', + SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE', HIDE_ERROR = 'HIDE_ERROR', CLEAR_ERROR = 'CLEAR_ERROR', RESET_AMOUNT = 'RESET_AMOUNT', @@ -45,7 +45,7 @@ export const actions = { 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), + setErrorMessage: (errorMessage: string) => createAction(ActionTypes.SET_ERROR_MESSAGE, errorMessage), hideError: () => createAction(ActionTypes.HIDE_ERROR), clearError: () => createAction(ActionTypes.CLEAR_ERROR), resetAmount: () => createAction(ActionTypes.RESET_AMOUNT), diff --git a/packages/instant/src/redux/reducer.ts b/packages/instant/src/redux/reducer.ts index 614ed21ac..dd9403052 100644 --- a/packages/instant/src/redux/reducer.ts +++ b/packages/instant/src/redux/reducer.ts @@ -29,8 +29,8 @@ export interface State { ethUsdPrice?: BigNumber; latestBuyQuote?: BuyQuote; quoteRequestState: AsyncProcessState; - latestError?: any; - latestErrorDisplay: DisplayStatus; + latestErrorMessage?: string; + latestErrorDisplayStatus: DisplayStatus; } export const INITIAL_STATE: State = { @@ -40,8 +40,8 @@ export const INITIAL_STATE: State = { buyOrderState: { processState: OrderProcessState.NONE }, ethUsdPrice: undefined, latestBuyQuote: undefined, - latestError: undefined, - latestErrorDisplay: DisplayStatus.Hidden, + latestErrorMessage: undefined, + latestErrorDisplayStatus: DisplayStatus.Hidden, quoteRequestState: AsyncProcessState.NONE, }; @@ -88,22 +88,22 @@ export const reducer = (state: State = INITIAL_STATE, action: Action): State => ...state, buyOrderState: action.data, }; - case ActionTypes.SET_ERROR: + case ActionTypes.SET_ERROR_MESSAGE: return { ...state, - latestError: action.data, - latestErrorDisplay: DisplayStatus.Present, + latestErrorMessage: action.data, + latestErrorDisplayStatus: DisplayStatus.Present, }; case ActionTypes.HIDE_ERROR: return { ...state, - latestErrorDisplay: DisplayStatus.Hidden, + latestErrorDisplayStatus: DisplayStatus.Hidden, }; case ActionTypes.CLEAR_ERROR: return { ...state, - latestError: undefined, - latestErrorDisplay: DisplayStatus.Hidden, + latestErrorMessage: undefined, + latestErrorDisplayStatus: DisplayStatus.Hidden, }; case ActionTypes.UPDATE_SELECTED_ASSET: const newSelectedAssetData = action.data; diff --git a/packages/instant/src/types.ts b/packages/instant/src/types.ts index c02b66990..336465e43 100644 --- a/packages/instant/src/types.ts +++ b/packages/instant/src/types.ts @@ -44,7 +44,7 @@ export interface ERC20AssetMetaData { export interface ERC721AssetMetaData { assetProxyId: AssetProxyId.ERC721; name: string; - representationUrl?: string; + imageUrl?: string; primaryColor?: string; } diff --git a/packages/instant/src/util/assert.ts b/packages/instant/src/util/assert.ts new file mode 100644 index 000000000..584d3d4b1 --- /dev/null +++ b/packages/instant/src/util/assert.ts @@ -0,0 +1,44 @@ +import { assert as sharedAssert } from '@0x/assert'; +import { schemas } from '@0x/json-schemas'; +import { assetDataUtils } from '@0x/order-utils'; +import { AssetProxyId, ObjectMap, SignedOrder } from '@0x/types'; +import * as _ from 'lodash'; + +import { AssetMetaData } from '../types'; + +export const assert = { + ...sharedAssert, + isValidLiquiditySource(variableName: string, liquiditySource: string | SignedOrder[]): void { + if (_.isString(liquiditySource)) { + sharedAssert.isUri(variableName, liquiditySource); + return; + } + sharedAssert.doesConformToSchema(variableName, liquiditySource, schemas.signedOrdersSchema); + }, + isValidAssetMetaDataMap(variableName: string, metaDataMap: ObjectMap<AssetMetaData>): void { + _.forEach(metaDataMap, (metaData, assetData) => { + assert.isHexString(`key ${assetData} of ${variableName}`, assetData); + assert.isValidAssetMetaData(`${variableName}.${assetData}`, metaData); + const assetDataProxyId = assetDataUtils.decodeAssetProxyId(assetData); + assert.assert( + metaData.assetProxyId === assetDataProxyId, + `Expected meta data for assetData ${assetData} to have asset proxy id of ${assetDataProxyId}, but instead got ${ + metaData.assetProxyId + }`, + ); + }); + }, + isValidAssetMetaData(variableName: string, metaData: AssetMetaData): void { + assert.isHexString(`${variableName}.assetProxyId`, metaData.assetProxyId); + if (!_.isUndefined(metaData.primaryColor)) { + assert.isString(`${variableName}.primaryColor`, metaData.primaryColor); + } + if (metaData.assetProxyId === AssetProxyId.ERC20) { + assert.isNumber(`${variableName}.decimals`, metaData.decimals); + assert.isString(`${variableName}.symbol`, metaData.symbol); + } else if (metaData.assetProxyId === AssetProxyId.ERC721) { + assert.isString(`${variableName}.name`, metaData.name); + assert.isUri(`${variableName}.imageUrl`, metaData.imageUrl); + } + }, +}; diff --git a/packages/instant/src/util/asset.ts b/packages/instant/src/util/asset.ts index 2c5b6325d..630103c7b 100644 --- a/packages/instant/src/util/asset.ts +++ b/packages/instant/src/util/asset.ts @@ -18,7 +18,10 @@ export const assetUtils = { getMetaDataOrThrow: (assetData: string, metaDataMap: ObjectMap<AssetMetaData>, network: Network): AssetMetaData => { let mainnetAssetData: string | undefined = assetData; if (network !== Network.Mainnet) { - mainnetAssetData = assetUtils.getAssociatedAssetDataIfExists(assetData, network); + const mainnetAssetDataIfExists = assetUtils.getAssociatedAssetDataIfExists(assetData, network); + // Just so we don't fail in the case where we are on a non-mainnet network, + // but pass in a valid mainnet assetData. + mainnetAssetData = mainnetAssetDataIfExists || assetData; } if (_.isUndefined(mainnetAssetData)) { throw new Error(ZeroExInstantError.AssetMetaDataNotAvailable); diff --git a/packages/instant/src/util/balance.ts b/packages/instant/src/util/balance.ts index 533656858..f2271495b 100644 --- a/packages/instant/src/util/balance.ts +++ b/packages/instant/src/util/balance.ts @@ -3,11 +3,11 @@ import { Web3Wrapper } from '@0x/web3-wrapper'; import * as _ from 'lodash'; export const balanceUtil = { - hasSufficentEth: async (takerAddress: string | undefined, buyQuote: BuyQuote, web3Wrapper: Web3Wrapper) => { + hasSufficientEth: async (takerAddress: string | undefined, buyQuote: BuyQuote, web3Wrapper: Web3Wrapper) => { if (_.isUndefined(takerAddress)) { return false; } const balanceWei = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - return balanceWei >= buyQuote.worstCaseQuoteInfo.totalEthAmount; + return balanceWei.gte(buyQuote.worstCaseQuoteInfo.totalEthAmount); }, }; diff --git a/packages/instant/src/util/big_number_input.ts b/packages/instant/src/util/big_number_input.ts index d2a9a8dc5..370d91a0a 100644 --- a/packages/instant/src/util/big_number_input.ts +++ b/packages/instant/src/util/big_number_input.ts @@ -10,14 +10,19 @@ import * as _ from 'lodash'; */ export class BigNumberInput extends BigNumber { private readonly _isEndingWithDecimal: boolean; - constructor(bigNumberString: string) { - const hasDecimalPeriod = _.endsWith(bigNumberString, '.'); - let internalString = bigNumberString; - if (hasDecimalPeriod) { - internalString = bigNumberString.slice(0, -1); + constructor(numberOrString: string | number) { + if (_.isString(numberOrString)) { + const hasDecimalPeriod = _.endsWith(numberOrString, '.'); + let internalString = numberOrString; + if (hasDecimalPeriod) { + internalString = numberOrString.slice(0, -1); + } + super(internalString); + this._isEndingWithDecimal = hasDecimalPeriod; + } else { + super(numberOrString); + this._isEndingWithDecimal = false; } - super(internalString); - this._isEndingWithDecimal = hasDecimalPeriod; } public toDisplayString(): string { const internalString = super.toString(); diff --git a/packages/instant/src/util/error.ts b/packages/instant/src/util/error.ts deleted file mode 100644 index 39c563c75..000000000 --- a/packages/instant/src/util/error.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { AssetBuyerError } from '@0x/asset-buyer'; -import { Dispatch } from 'redux'; - -import { Action, actions } from '../redux/actions'; -import { Asset, ZeroExInstantError } from '../types'; - -import { assetUtils } from './asset'; - -class ErrorFlasher { - private _timeoutId?: number; - public flashNewError(dispatch: Dispatch<Action>, error: any, delayMs: number = 7000): void { - this._clearTimeout(); - - // dispatch new message - dispatch(actions.setError(error)); - - this._timeoutId = window.setTimeout(() => { - dispatch(actions.hideError()); - }, delayMs); - } - public clearError(dispatch: Dispatch<Action>): void { - this._clearTimeout(); - dispatch(actions.hideError()); - } - private _clearTimeout(): void { - if (this._timeoutId) { - window.clearTimeout(this._timeoutId); - } - } -} - -const humanReadableMessageForError = (error: Error, asset?: Asset): string | undefined => { - const hasInsufficientLiquidity = - error.message === AssetBuyerError.InsufficientAssetLiquidity || - error.message === AssetBuyerError.InsufficientZrxLiquidity; - if (hasInsufficientLiquidity) { - const assetName = assetUtils.bestNameForAsset(asset, 'of this asset'); - return `Not enough ${assetName} available`; - } - - if ( - error.message === AssetBuyerError.StandardRelayerApiError || - error.message.startsWith(AssetBuyerError.AssetUnavailable) - ) { - const assetName = assetUtils.bestNameForAsset(asset, 'This asset'); - return `${assetName} is currently unavailable`; - } - - if (error.message === AssetBuyerError.SignatureRequestDenied) { - return 'You denied this transaction'; - } - if (error.message === ZeroExInstantError.InsufficientETH) { - return "You don't have enough ETH"; - } - - return undefined; -}; - -export const errorUtil = { - errorFlasher: new ErrorFlasher(), - errorDescription: (error?: any, asset?: Asset): { icon: string; message: string } => { - let bestMessage: string | undefined; - if (error instanceof Error) { - bestMessage = humanReadableMessageForError(error, asset); - } - return { - icon: '😢', - message: bestMessage || 'Something went wrong...', - }; - }, -}; diff --git a/packages/instant/src/util/error_flasher.ts b/packages/instant/src/util/error_flasher.ts new file mode 100644 index 000000000..068c12fe2 --- /dev/null +++ b/packages/instant/src/util/error_flasher.ts @@ -0,0 +1,26 @@ +import { Dispatch } from 'redux'; + +import { Action, actions } from '../redux/actions'; + +class ErrorFlasher { + private _timeoutId?: number; + public flashNewErrorMessage(dispatch: Dispatch<Action>, errorMessage?: string, delayMs: number = 7000): void { + this._clearTimeout(); + // dispatch new message + dispatch(actions.setErrorMessage(errorMessage || 'Something went wrong...')); + this._timeoutId = window.setTimeout(() => { + dispatch(actions.hideError()); + }, delayMs); + } + public clearError(dispatch: Dispatch<Action>): void { + this._clearTimeout(); + dispatch(actions.hideError()); + } + private _clearTimeout(): void { + if (this._timeoutId) { + window.clearTimeout(this._timeoutId); + } + } +} + +export const errorFlasher = new ErrorFlasher(); diff --git a/packages/instant/test/util/error.test.ts b/packages/instant/test/util/error.test.ts deleted file mode 100644 index 90e9c5fb4..000000000 --- a/packages/instant/test/util/error.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -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).message).toEqual( - 'Not enough ZRX available', - ); - }); - it('should return error default name for InsufficientAssetLiquidity', () => { - const insufficientZrxError = new Error(AssetBuyerError.InsufficientZrxLiquidity); - expect(errorUtil.errorDescription(insufficientZrxError).message).toEqual( - 'Not enough of this asset available', - ); - }); - it('should return asset name for InsufficientAssetLiquidity', () => { - const insufficientZrxError = new Error(AssetBuyerError.InsufficientZrxLiquidity); - 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).message).toEqual( - 'ZRX is currently unavailable', - ); - }); - it('should return error for AssetUnavailable error', () => { - 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, undefined).message).toEqual( - 'This asset is currently unavailable', - ); - }); - }); -}); @@ -1900,10 +1900,6 @@ aes-js@^0.2.3: version "0.2.4" resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-0.2.4.tgz#94b881ab717286d015fa219e08fb66709dda5a3d" -aes-js@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/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" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" @@ -3341,7 +3337,7 @@ bs-logger@0.x: dependencies: fast-json-stable-stringify "^2.0.0" -bs58@=4.0.1, bs58@^4.0.0: +bs58@=4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a" dependencies: @@ -3364,14 +3360,6 @@ bs58check@^1.0.8: bs58 "^3.1.0" create-hash "^1.1.0" -bs58check@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-2.1.2.tgz#53b018291228d82a5aa08e7d796fdafda54aebfc" - dependencies: - bs58 "^4.0.0" - create-hash "^1.1.0" - safe-buffer "^5.1.2" - bser@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719" @@ -5959,19 +5947,6 @@ ethereumjs-wallet@0.6.0: utf8 "^2.1.1" uuid "^2.0.1" -ethereumjs-wallet@~0.6.0: - version "0.6.2" - resolved "https://registry.yarnpkg.com/ethereumjs-wallet/-/ethereumjs-wallet-0.6.2.tgz#67244b6af3e8113b53d709124b25477b64aeccda" - dependencies: - aes-js "^3.1.1" - bs58check "^2.1.2" - ethereumjs-util "^5.2.0" - hdkey "^1.0.0" - safe-buffer "^5.1.2" - scrypt.js "^0.2.0" - utf8 "^3.0.0" - uuid "^3.3.2" - ethers@~4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/ethers/-/ethers-4.0.4.tgz#d3f85e8b27f4b59537e06526439b0fb15b44dc65" @@ -7501,14 +7476,6 @@ hdkey@^0.7.0, hdkey@^0.7.1: coinstring "^2.0.0" secp256k1 "^3.0.1" -hdkey@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/hdkey/-/hdkey-1.1.0.tgz#e74e7b01d2c47f797fa65d1d839adb7a44639f29" - dependencies: - coinstring "^2.0.0" - safe-buffer "^5.1.1" - secp256k1 "^3.0.1" - he@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" @@ -15595,10 +15562,6 @@ utf8@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/utf8/-/utf8-2.1.2.tgz#1fa0d9270e9be850d9b05027f63519bf46457d96" -utf8@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/utf8/-/utf8-3.0.0.tgz#f052eed1364d696e769ef058b183df88c87f69d1" - util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" |