diff options
-rw-r--r-- | packages/instant/src/components/buy_button.tsx | 59 | ||||
-rw-r--r-- | packages/instant/src/components/instant_heading.tsx | 39 | ||||
-rw-r--r-- | packages/instant/src/components/zero_ex_instant.tsx | 2 | ||||
-rw-r--r-- | packages/instant/src/components/zero_ex_instant_container.tsx | 7 | ||||
-rw-r--r-- | packages/instant/src/constants.ts | 4 | ||||
-rw-r--r-- | packages/instant/src/containers/selected_asset_amount_input.ts | 57 | ||||
-rw-r--r-- | packages/instant/src/containers/selected_asset_amount_input.tsx | 36 | ||||
-rw-r--r-- | packages/instant/src/containers/selected_asset_buy_button.ts | 59 | ||||
-rw-r--r-- | packages/instant/src/containers/selected_asset_instant_heading.ts | 26 | ||||
-rw-r--r-- | packages/instant/src/redux/async_data.ts | 22 | ||||
-rw-r--r-- | packages/instant/src/redux/reducer.ts | 19 | ||||
-rw-r--r-- | packages/instant/src/types.ts | 10 | ||||
-rw-r--r-- | packages/instant/src/util/asset_buyer.ts | 11 | ||||
-rw-r--r-- | packages/instant/src/util/coinbase_api.ts | 8 | ||||
-rw-r--r-- | packages/instant/src/util/provider.ts | 12 | ||||
-rw-r--r-- | packages/instant/src/util/web3_wrapper.ts | 5 |
16 files changed, 321 insertions, 55 deletions
diff --git a/packages/instant/src/components/buy_button.tsx b/packages/instant/src/components/buy_button.tsx index 5a32b9575..097b8e547 100644 --- a/packages/instant/src/components/buy_button.tsx +++ b/packages/instant/src/components/buy_button.tsx @@ -1,19 +1,56 @@ +import { BuyQuote } from '@0xproject/asset-buyer'; +import * as _ from 'lodash'; import * as React from 'react'; import { ColorOption } from '../style/theme'; +import { assetBuyer } from '../util/asset_buyer'; +import { web3Wrapper } from '../util/web3_wrapper'; import { Button, Container, Text } from './ui'; -export interface BuyButtonProps {} +export interface BuyButtonProps { + buyQuote?: BuyQuote; + onClick: (buyQuote: BuyQuote) => void; + onBuySuccess: (buyQuote: BuyQuote) => void; + onBuyFailure: (buyQuote: BuyQuote) => void; + text: string; +} -export const BuyButton: React.StatelessComponent<BuyButtonProps> = props => ( - <Container padding="20px" width="100%"> - <Button width="100%"> - <Text fontColor={ColorOption.white} fontWeight={600} fontSize="20px"> - Buy - </Text> - </Button> - </Container> -); +const boundNoop = _.noop.bind(_); -BuyButton.displayName = 'BuyButton'; +export class BuyButton extends React.Component<BuyButtonProps> { + public static defaultProps = { + onClick: boundNoop, + onBuySuccess: boundNoop, + onBuyFailure: boundNoop, + }; + public render(): React.ReactNode { + const shouldDisableButton = _.isUndefined(this.props.buyQuote); + return ( + <Container padding="20px" width="100%"> + <Button width="100%" onClick={this._handleClick} isDisabled={shouldDisableButton}> + <Text fontColor={ColorOption.white} fontWeight={600} fontSize="20px"> + {this.props.text} + </Text> + </Button> + </Container> + ); + } + private readonly _handleClick = async () => { + // The button is disabled when there is no buy quote anyway. + if (_.isUndefined(this.props.buyQuote)) { + return; + } + this.props.onClick(this.props.buyQuote); + try { + const txnHash = await assetBuyer.executeBuyQuoteAsync(this.props.buyQuote, { + // HACK: There is a calculation issue in asset-buyer. ETH is refunded anyway so just over-estimate. + ethAmount: this.props.buyQuote.worstCaseQuoteInfo.totalEthAmount.mul(2), + }); + await web3Wrapper.awaitTransactionSuccessAsync(txnHash); + } catch { + this.props.onBuyFailure(this.props.buyQuote); + } + this.props.onBuySuccess(this.props.buyQuote); + }; +} diff --git a/packages/instant/src/components/instant_heading.tsx b/packages/instant/src/components/instant_heading.tsx index be0414b8d..cde3862c7 100644 --- a/packages/instant/src/components/instant_heading.tsx +++ b/packages/instant/src/components/instant_heading.tsx @@ -1,11 +1,42 @@ +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import * as _ from 'lodash'; import * as React from 'react'; +import { ethDecimals } from '../constants'; import { SelectedAssetAmountInput } from '../containers/selected_asset_amount_input'; import { ColorOption } from '../style/theme'; import { Container, Flex, Text } from './ui'; -export interface InstantHeadingProps {} +export interface InstantHeadingProps { + selectedAssetAmount?: BigNumber; + totalEthBaseAmount?: BigNumber; + ethUsdPrice?: BigNumber; +} + +const displaytotalEthBaseAmount = ({ selectedAssetAmount, totalEthBaseAmount }: InstantHeadingProps): string => { + if (_.isUndefined(selectedAssetAmount)) { + return '0 ETH'; + } + if (_.isUndefined(totalEthBaseAmount)) { + return '...loading'; + } + const ethUnitAmount = Web3Wrapper.toUnitAmount(totalEthBaseAmount, ethDecimals); + const roundedAmount = ethUnitAmount.round(4); + return `${roundedAmount} ETH`; +}; + +const displayUsdAmount = ({ totalEthBaseAmount, selectedAssetAmount, ethUsdPrice }: InstantHeadingProps): string => { + if (_.isUndefined(selectedAssetAmount)) { + return '$0.00'; + } + if (_.isUndefined(totalEthBaseAmount) || _.isUndefined(ethUsdPrice)) { + return '...loading'; + } + const ethUnitAmount = Web3Wrapper.toUnitAmount(totalEthBaseAmount, ethDecimals); + return `$${ethUnitAmount.mul(ethUsdPrice).round(2)}`; +}; export const InstantHeading: React.StatelessComponent<InstantHeadingProps> = props => ( <Container backgroundColor={ColorOption.primaryColor} padding="20px" width="100%" borderRadius="3px 3px 0px 0px"> @@ -26,18 +57,18 @@ export const InstantHeading: React.StatelessComponent<InstantHeadingProps> = pro <SelectedAssetAmountInput fontSize="45px" /> <Container display="inline-block" marginLeft="10px"> <Text fontSize="45px" fontColor={ColorOption.white} textTransform="uppercase"> - rep + zrx </Text> </Container> </Container> <Flex direction="column" justify="space-between"> <Container marginBottom="5px"> <Text fontSize="16px" fontColor={ColorOption.white} fontWeight={500}> - 0 ETH + {displaytotalEthBaseAmount(props)} </Text> </Container> <Text fontSize="16px" fontColor={ColorOption.white} opacity={0.7}> - $0.00 + {displayUsdAmount(props)} </Text> </Flex> </Flex> diff --git a/packages/instant/src/components/zero_ex_instant.tsx b/packages/instant/src/components/zero_ex_instant.tsx index 0e6230d1b..17ef737c9 100644 --- a/packages/instant/src/components/zero_ex_instant.tsx +++ b/packages/instant/src/components/zero_ex_instant.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { Provider } from 'react-redux'; +import { asyncData } from '../redux/async_data'; import { store } from '../redux/store'; import { fonts } from '../style/fonts'; import { theme, ThemeProvider } from '../style/theme'; @@ -8,6 +9,7 @@ import { theme, ThemeProvider } from '../style/theme'; import { ZeroExInstantContainer } from './zero_ex_instant_container'; fonts.include(); +asyncData.fetchAndDispatchToStore(); export interface ZeroExInstantProps {} diff --git a/packages/instant/src/components/zero_ex_instant_container.tsx b/packages/instant/src/components/zero_ex_instant_container.tsx index 716227b51..4ff8b51bd 100644 --- a/packages/instant/src/components/zero_ex_instant_container.tsx +++ b/packages/instant/src/components/zero_ex_instant_container.tsx @@ -1,5 +1,8 @@ import * as React from 'react'; +import { SelectedAssetBuyButton } from '../containers/selected_asset_buy_button'; +import { SelectedAssetInstantHeading } from '../containers/selected_asset_instant_heading'; + import { ColorOption } from '../style/theme'; import { BuyButton } from './buy_button'; @@ -12,9 +15,9 @@ export interface ZeroExInstantContainerProps {} export const ZeroExInstantContainer: React.StatelessComponent<ZeroExInstantContainerProps> = props => ( <Container hasBoxShadow={true} width="350px" backgroundColor={ColorOption.white} borderRadius="3px"> <Flex direction="column" justify="flex-start"> - <InstantHeading /> + <SelectedAssetInstantHeading /> <OrderDetails /> - <BuyButton /> + <SelectedAssetBuyButton /> </Flex> </Container> ); diff --git a/packages/instant/src/constants.ts b/packages/instant/src/constants.ts new file mode 100644 index 000000000..397c9b07a --- /dev/null +++ b/packages/instant/src/constants.ts @@ -0,0 +1,4 @@ +export const sraApiUrl = 'https://api.radarrelay.com/0x/v2/'; +export const zrxContractAddress = '0xe41d2489571d322189246dafa5ebde1f4699f498'; +export const zrxDecimals = 18; +export const ethDecimals = 18; diff --git a/packages/instant/src/containers/selected_asset_amount_input.ts b/packages/instant/src/containers/selected_asset_amount_input.ts new file mode 100644 index 000000000..b731b889a --- /dev/null +++ b/packages/instant/src/containers/selected_asset_amount_input.ts @@ -0,0 +1,57 @@ +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import * as _ from 'lodash'; +import * as React from 'react'; +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; + +import { zrxContractAddress, zrxDecimals } from '../constants'; +import { State } from '../redux/reducer'; +import { ColorOption } from '../style/theme'; +import { Action, ActionTypes, AsyncProcessState } from '../types'; +import { assetBuyer } from '../util/asset_buyer'; + +import { AmountInput } from '../components/amount_input'; + +export interface SelectedAssetAmountInputProps { + fontColor?: ColorOption; + fontSize?: string; +} + +interface ConnectedState { + value?: BigNumber; +} + +interface ConnectedDispatch { + onChange?: (value?: BigNumber) => void; +} + +const mapStateToProps = (state: State, _ownProps: SelectedAssetAmountInputProps): ConnectedState => ({ + value: state.selectedAssetAmount, +}); + +const mapDispatchToProps = (dispatch: Dispatch<Action>): ConnectedDispatch => ({ + onChange: async value => { + // Update the input + dispatch({ type: ActionTypes.UPDATE_SELECTED_ASSET_AMOUNT, data: value }); + // invalidate the last buy quote. + dispatch({ type: ActionTypes.UPDATE_LATEST_BUY_QUOTE, data: undefined }); + // reset our buy state + dispatch({ type: ActionTypes.UPDATE_SELECTED_ASSET_BUY_STATE, data: AsyncProcessState.NONE }); + if (!_.isUndefined(value)) { + // get a new buy quote. + const baseUnitValue = Web3Wrapper.toBaseUnitAmount(value, zrxDecimals); + const newBuyQuote = await assetBuyer.getBuyQuoteForERC20TokenAddressAsync( + zrxContractAddress, + baseUnitValue, + ); + // invalidate the last buy quote. + dispatch({ type: ActionTypes.UPDATE_LATEST_BUY_QUOTE, data: newBuyQuote }); + } + }, +}); + +export const SelectedAssetAmountInput: React.ComponentClass<SelectedAssetAmountInputProps> = connect( + mapStateToProps, + mapDispatchToProps, +)(AmountInput); diff --git a/packages/instant/src/containers/selected_asset_amount_input.tsx b/packages/instant/src/containers/selected_asset_amount_input.tsx deleted file mode 100644 index 800a4c568..000000000 --- a/packages/instant/src/containers/selected_asset_amount_input.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { BigNumber } from '@0xproject/utils'; -import * as React from 'react'; -import { connect } from 'react-redux'; -import { Dispatch } from 'redux'; - -import { State } from '../redux/reducer'; -import { ColorOption } from '../style/theme'; -import { Action, ActionTypes } from '../types'; - -import { AmountInput } from '../components/amount_input'; - -export interface SelectedAssetAmountInputProps { - fontColor?: ColorOption; - fontSize?: string; -} - -interface ConnectedState { - value?: BigNumber; -} - -interface ConnectedDispatch { - onChange?: (value?: BigNumber) => void; -} - -const mapStateToProps = (state: State, _ownProps: SelectedAssetAmountInputProps): ConnectedState => ({ - value: state.selectedAssetAmount, -}); - -const mapDispatchToProps = (dispatch: Dispatch<Action>): ConnectedDispatch => ({ - onChange: value => dispatch({ type: ActionTypes.UPDATE_SELECTED_ASSET_AMOUNT, data: value }), -}); - -export const SelectedAssetAmountInput: React.ComponentClass<SelectedAssetAmountInputProps> = connect( - mapStateToProps, - mapDispatchToProps, -)(AmountInput); diff --git a/packages/instant/src/containers/selected_asset_buy_button.ts b/packages/instant/src/containers/selected_asset_buy_button.ts new file mode 100644 index 000000000..f537294e4 --- /dev/null +++ b/packages/instant/src/containers/selected_asset_buy_button.ts @@ -0,0 +1,59 @@ +import { BuyQuote } from '@0xproject/asset-buyer'; +import * as _ from 'lodash'; +import * as React from 'react'; +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; + +import { State } from '../redux/reducer'; +import { Action, ActionTypes, AsyncProcessState } from '../types'; +import { assetBuyer } from '../util/asset_buyer'; +import { web3Wrapper } from '../util/web3_wrapper'; + +import { BuyButton } from '../components/buy_button'; + +export interface SelectedAssetBuyButtonProps {} + +interface ConnectedState { + text: string; + buyQuote?: BuyQuote; +} + +interface ConnectedDispatch { + onClick: (buyQuote: BuyQuote) => void; + onBuySuccess: (buyQuote: BuyQuote) => void; + onBuyFailure: (buyQuote: BuyQuote) => void; +} + +const textForState = (state: AsyncProcessState): string => { + switch (state) { + case AsyncProcessState.NONE: + return 'Buy'; + case AsyncProcessState.PENDING: + return '...Loading'; + case AsyncProcessState.SUCCESS: + return 'Success!'; + case AsyncProcessState.FAILURE: + return 'Failed'; + default: + return 'Buy'; + } +}; + +const mapStateToProps = (state: State, _ownProps: SelectedAssetBuyButtonProps): ConnectedState => ({ + text: textForState(state.selectedAssetBuyState), + buyQuote: state.latestBuyQuote, +}); + +const mapDispatchToProps = (dispatch: Dispatch<Action>, ownProps: SelectedAssetBuyButtonProps): ConnectedDispatch => ({ + onClick: buyQuote => + dispatch({ type: ActionTypes.UPDATE_SELECTED_ASSET_BUY_STATE, data: AsyncProcessState.PENDING }), + onBuySuccess: buyQuote => + dispatch({ type: ActionTypes.UPDATE_SELECTED_ASSET_BUY_STATE, data: AsyncProcessState.SUCCESS }), + onBuyFailure: buyQuote => + dispatch({ type: ActionTypes.UPDATE_SELECTED_ASSET_BUY_STATE, data: AsyncProcessState.FAILURE }), +}); + +export const SelectedAssetBuyButton: React.ComponentClass<SelectedAssetBuyButtonProps> = connect( + mapStateToProps, + mapDispatchToProps, +)(BuyButton); diff --git a/packages/instant/src/containers/selected_asset_instant_heading.ts b/packages/instant/src/containers/selected_asset_instant_heading.ts new file mode 100644 index 000000000..a9e853fe1 --- /dev/null +++ b/packages/instant/src/containers/selected_asset_instant_heading.ts @@ -0,0 +1,26 @@ +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; +import * as React from 'react'; +import { connect } from 'react-redux'; + +import { State } from '../redux/reducer'; + +import { InstantHeading } from '../components/instant_heading'; + +export interface InstantHeadingProps {} + +interface ConnectedState { + selectedAssetAmount?: BigNumber; + totalEthBaseAmount?: BigNumber; + ethUsdPrice?: BigNumber; +} + +const mapStateToProps = (state: State, _ownProps: InstantHeadingProps): ConnectedState => ({ + selectedAssetAmount: state.selectedAssetAmount, + totalEthBaseAmount: _.get(state, 'latestBuyQuote.worstCaseQuoteInfo.totalEthAmount'), + ethUsdPrice: state.ethUsdPrice, +}); + +export const SelectedAssetInstantHeading: React.ComponentClass<InstantHeadingProps> = connect(mapStateToProps)( + InstantHeading, +); diff --git a/packages/instant/src/redux/async_data.ts b/packages/instant/src/redux/async_data.ts new file mode 100644 index 000000000..3fde2d2e5 --- /dev/null +++ b/packages/instant/src/redux/async_data.ts @@ -0,0 +1,22 @@ +import { BigNumber } from '@0xproject/utils'; + +import { ActionTypes } from '../types'; +import { coinbaseApi } from '../util/coinbase_api'; + +import { store } from './store'; + +export const asyncData = { + fetchAndDispatchToStore: async () => { + let ethUsdPriceStr = '0'; + try { + ethUsdPriceStr = await coinbaseApi.getEthUsdPrice(); + } catch (e) { + // ignore + } finally { + store.dispatch({ + type: ActionTypes.UPDATE_ETH_USD_PRICE, + data: new BigNumber(ethUsdPriceStr), + }); + } + }, +}; diff --git a/packages/instant/src/redux/reducer.ts b/packages/instant/src/redux/reducer.ts index 5026895ae..c0c4b06ad 100644 --- a/packages/instant/src/redux/reducer.ts +++ b/packages/instant/src/redux/reducer.ts @@ -1,16 +1,21 @@ +import { BuyQuote } from '@0xproject/asset-buyer'; import { BigNumber } from '@0xproject/utils'; import * as _ from 'lodash'; -import { Action, ActionTypes } from '../types'; +import { Action, ActionTypes, AsyncProcessState } from '../types'; export interface State { - ethUsdPrice?: string; selectedAssetAmount?: BigNumber; + selectedAssetBuyState: AsyncProcessState; + ethUsdPrice?: BigNumber; + latestBuyQuote?: BuyQuote; } export const INITIAL_STATE: State = { ethUsdPrice: undefined, + selectedAssetBuyState: AsyncProcessState.NONE, selectedAssetAmount: undefined, + latestBuyQuote: undefined, }; export const reducer = (state: State = INITIAL_STATE, action: Action): State => { @@ -25,6 +30,16 @@ export const reducer = (state: State = INITIAL_STATE, action: Action): State => ...state, selectedAssetAmount: action.data, }; + case ActionTypes.UPDATE_LATEST_BUY_QUOTE: + return { + ...state, + latestBuyQuote: action.data, + }; + case ActionTypes.UPDATE_SELECTED_ASSET_BUY_STATE: + return { + ...state, + selectedAssetBuyState: action.data, + }; default: return state; } diff --git a/packages/instant/src/types.ts b/packages/instant/src/types.ts index d150bd8ab..bbf31cbf4 100644 --- a/packages/instant/src/types.ts +++ b/packages/instant/src/types.ts @@ -1,6 +1,16 @@ +// Reusable +export enum AsyncProcessState { + NONE, + PENDING, + SUCCESS, + FAILURE, +} + export enum ActionTypes { UPDATE_ETH_USD_PRICE, UPDATE_SELECTED_ASSET_AMOUNT, + UPDATE_SELECTED_ASSET_BUY_STATE, + UPDATE_LATEST_BUY_QUOTE, } export interface Action { diff --git a/packages/instant/src/util/asset_buyer.ts b/packages/instant/src/util/asset_buyer.ts new file mode 100644 index 000000000..b030501c4 --- /dev/null +++ b/packages/instant/src/util/asset_buyer.ts @@ -0,0 +1,11 @@ +import { AssetBuyer } from '@0xproject/asset-buyer'; + +import { sraApiUrl } from '../constants'; + +import { getProvider } from './provider'; + +const provider = getProvider(); + +export const assetBuyer = AssetBuyer.getAssetBuyerForStandardRelayerAPIUrl(provider, sraApiUrl, { + expiryBufferSeconds: 300, +}); diff --git a/packages/instant/src/util/coinbase_api.ts b/packages/instant/src/util/coinbase_api.ts new file mode 100644 index 000000000..63c8077da --- /dev/null +++ b/packages/instant/src/util/coinbase_api.ts @@ -0,0 +1,8 @@ +const baseEndpoint = 'https://api.coinbase.com/v2'; +export const coinbaseApi = { + getEthUsdPrice: async (): Promise<string> => { + const res = await fetch(`${baseEndpoint}/prices/ETH-USD/buy`); + const resJson = await res.json(); + return resJson.data.amount; + }, +}; diff --git a/packages/instant/src/util/provider.ts b/packages/instant/src/util/provider.ts new file mode 100644 index 000000000..49705fd11 --- /dev/null +++ b/packages/instant/src/util/provider.ts @@ -0,0 +1,12 @@ +import { Provider } from 'ethereum-types'; + +export const getProvider = (): Provider => { + const injectedWeb3 = (window as any).web3 || undefined; + try { + // Use MetaMask/Mist provider + return injectedWeb3.currentProvider; + } catch (err) { + // Throws when user doesn't have MetaMask/Mist running + throw new Error(`No injected web3 found: ${err}`); + } +}; diff --git a/packages/instant/src/util/web3_wrapper.ts b/packages/instant/src/util/web3_wrapper.ts new file mode 100644 index 000000000..d7e43521f --- /dev/null +++ b/packages/instant/src/util/web3_wrapper.ts @@ -0,0 +1,5 @@ +import { Web3Wrapper } from '@0xproject/web3-wrapper'; + +import { getProvider } from './provider'; + +export const web3Wrapper = new Web3Wrapper(getProvider()); |