feat: Move over features from zrx-buyer

This commit is contained in:
fragosti
2018-10-10 18:27:06 -07:00
parent a5a033c359
commit 19f61906d3
16 changed files with 321 additions and 55 deletions

View File

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

View File

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

View File

@@ -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 {}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 {

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
import { Web3Wrapper } from '@0xproject/web3-wrapper';
import { getProvider } from './provider';
export const web3Wrapper = new Web3Wrapper(getProvider());