feat: Move over features from zrx-buyer
This commit is contained in:
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
4
packages/instant/src/constants.ts
Normal file
4
packages/instant/src/constants.ts
Normal 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;
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
59
packages/instant/src/containers/selected_asset_buy_button.ts
Normal file
59
packages/instant/src/containers/selected_asset_buy_button.ts
Normal 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);
|
||||
@@ -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,
|
||||
);
|
||||
22
packages/instant/src/redux/async_data.ts
Normal file
22
packages/instant/src/redux/async_data.ts
Normal 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),
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
11
packages/instant/src/util/asset_buyer.ts
Normal file
11
packages/instant/src/util/asset_buyer.ts
Normal 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,
|
||||
});
|
||||
8
packages/instant/src/util/coinbase_api.ts
Normal file
8
packages/instant/src/util/coinbase_api.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
12
packages/instant/src/util/provider.ts
Normal file
12
packages/instant/src/util/provider.ts
Normal 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}`);
|
||||
}
|
||||
};
|
||||
5
packages/instant/src/util/web3_wrapper.ts
Normal file
5
packages/instant/src/util/web3_wrapper.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Web3Wrapper } from '@0xproject/web3-wrapper';
|
||||
|
||||
import { getProvider } from './provider';
|
||||
|
||||
export const web3Wrapper = new Web3Wrapper(getProvider());
|
||||
Reference in New Issue
Block a user