src/modules/trading/components/trading--form/trading--form.jsx
/* eslint jsx-a11y/label-has-for: 0 */
import { augur } from "services/augurjs";
import React, { Component } from "react";
import PropTypes from "prop-types";
import classNames from "classnames";
import { BigNumber, createBigNumber } from "utils/create-big-number";
import { MARKET, LIMIT } from "modules/transactions/constants/types";
import { ZERO, MIN_QUANTITY } from "modules/trades/constants/numbers";
import {
YES_NO,
CATEGORICAL,
SCALAR
} from "modules/markets/constants/market-types";
import { isEqual } from "lodash";
import ReactTooltip from "react-tooltip";
import TooltipStyles from "modules/common/less/tooltip.styles";
import {
Hint,
ExclamationCircle as InputErrorIcon
} from "modules/common/components/icons";
import FormStyles from "modules/common/less/form";
import Styles from "modules/trading/components/trading--form/trading--form.styles";
import {
formatEther,
formatShares,
formatGasCostToEther
} from "utils/format-number";
import Checkbox from "src/modules/common/components/checkbox/checkbox";
class MarketTradingForm extends Component {
static propTypes = {
availableFunds: PropTypes.instanceOf(BigNumber).isRequired,
isMobile: PropTypes.bool.isRequired,
market: PropTypes.object.isRequired,
marketQuantity: PropTypes.string.isRequired,
marketOrderTotal: PropTypes.string.isRequired,
marketType: PropTypes.string.isRequired,
maxPrice: PropTypes.instanceOf(BigNumber).isRequired,
minPrice: PropTypes.instanceOf(BigNumber).isRequired,
nextPage: PropTypes.func.isRequired,
orderEthEstimate: PropTypes.string.isRequired,
orderShareEstimate: PropTypes.string.isRequired,
orderPrice: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.object
]).isRequired,
orderQuantity: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.object
]).isRequired,
orderType: PropTypes.string.isRequired,
selectedNav: PropTypes.string.isRequired,
selectedOutcome: PropTypes.object.isRequired,
updateState: PropTypes.func.isRequired,
doNotCreateOrders: PropTypes.bool.isRequired,
gasPrice: PropTypes.number.isRequired
};
constructor(props) {
super(props);
this.INPUT_TYPES = {
QUANTITY: "orderQuantity",
PRICE: "orderPrice",
MARKET_ORDER_SIZE: "marketOrderTotal",
DO_NOT_CREATE_ORDERS: "doNotCreateOrders"
};
this.gas = {
fillGasLimit: augur.constants.WORST_CASE_FILL[props.market.numOutcomes],
placeOrderNoSharesGasLimit:
augur.constants.PLACE_ORDER_NO_SHARES[props.market.numOutcomes],
placeOrderWithSharesGasLimit:
augur.constants.PLACE_ORDER_WITH_SHARES[props.market.numOutcomes]
};
this.TRADE_MAX_COST = "tradeMaxCost";
this.MINIMUM_TRADE_VALUE = createBigNumber(1, 10).dividedBy(10000);
this.orderValidation = this.orderValidation.bind(this);
this.testQuantity = this.testQuantity.bind(this);
this.testPrice = this.testPrice.bind(this);
this.updateTrade = this.updateTrade.bind(this);
const startState = {
[this.INPUT_TYPES.QUANTITY]: props.orderQuantity,
[this.INPUT_TYPES.PRICE]: props.orderPrice,
[this.INPUT_TYPES.MARKET_ORDER_SIZE]: props.marketOrderTotal,
[this.INPUT_TYPES.DO_NOT_CREATE_ORDERS]: props.doNotCreateOrders,
errors: {
[this.INPUT_TYPES.QUANTITY]: [],
[this.INPUT_TYPES.PRICE]: [],
[this.INPUT_TYPES.MARKET_ORDER_SIZE]: [],
[this.TRADE_MAX_COST]: []
}
};
this.state = {
...startState,
isOrderValid: this.orderValidation(startState).isOrderValid
};
}
componentWillReceiveProps(nextProps) {
const {
orderEthEstimate,
orderShareEstimate,
selectedNav,
selectedOutcome,
updateState
} = this.props;
// make sure to keep Quantity and Price as bigNumbers
const nextQuantity = nextProps[this.INPUT_TYPES.QUANTITY];
const nextPrice = nextProps[this.INPUT_TYPES.PRICE];
const newStateInfo = {
[this.INPUT_TYPES.QUANTITY]: nextQuantity
? createBigNumber(nextQuantity, 10)
: nextQuantity,
[this.INPUT_TYPES.PRICE]:
nextPrice && nextPrice !== ""
? createBigNumber(nextPrice, 10)
: nextPrice,
[this.INPUT_TYPES.MARKET_ORDER_SIZE]:
nextProps[this.INPUT_TYPES.MARKET_ORDER_SIZE],
[this.INPUT_TYPES.DO_NOT_CREATE_ORDERS]:
nextProps[this.INPUT_TYPES.DO_NOT_CREATE_ORDERS]
};
const currentStateInfo = {
[this.INPUT_TYPES.QUANTITY]: this.state[this.INPUT_TYPES.QUANTITY],
[this.INPUT_TYPES.PRICE]: this.state[this.INPUT_TYPES.PRICE],
[this.INPUT_TYPES.MARKET_ORDER_SIZE]: this.state[
this.INPUT_TYPES.MARKET_ORDER_SIZE
],
[this.INPUT_TYPES.DO_NOT_CREATE_ORDERS]: this.state[
this.INPUT_TYPES.DO_NOT_CREATE_ORDERS
]
};
const newOrderInfo = {
orderEthEstimate: nextProps.orderEthEstimate,
orderShareEstimate: nextProps.orderShareEstimate,
selectedNav: nextProps.selectedNav,
...newStateInfo
};
const currentOrderInfo = {
orderEthEstimate,
orderShareEstimate,
selectedNav,
...currentStateInfo
};
if (!isEqual(newOrderInfo, currentOrderInfo)) {
// trade has changed, lets update trade.
this.updateTrade(newStateInfo, nextProps);
const nextTradePrice = nextProps.selectedOutcome.trade.limitPrice;
const prevTradePrice = selectedOutcome.trade.limitPrice;
// limitPrice is being defaulted and we had no value in the input box
const priceChange = prevTradePrice === null && nextTradePrice !== null;
// limitPrice is being updated in the background, but we have no limitPrice input set.
const forcePriceUpdate =
prevTradePrice === nextTradePrice &&
nextTradePrice !== null &&
isNaN(
this.state[this.INPUT_TYPES.PRICE] &&
createBigNumber(this.state[this.INPUT_TYPES.PRICE], 10)
) &&
isNaN(
nextProps[this.INPUT_TYPES.PRICE] &&
createBigNumber(nextProps[this.INPUT_TYPES.PRICE], 10)
);
if (priceChange || forcePriceUpdate) {
// if limitPrice input hasn't been changed and we have defaulted the limitPrice, populate the field so as to not confuse the user as to where estimates are coming from.
updateState(
this.INPUT_TYPES.PRICE,
createBigNumber(nextTradePrice, 10)
);
}
// orderValidation
const { isOrderValid, errors, errorCount } = this.orderValidation(
newStateInfo,
nextProps
);
// update state
this.setState({ ...newStateInfo, errors, isOrderValid, errorCount });
}
}
testQuantity(value, errors, isOrderValid) {
let errorCount = 0;
let passedTest = !!isOrderValid;
if (!BigNumber.isBigNumber(value))
return { isOrderValid: false, errors, errorCount };
if (value && value.lte(0)) {
errorCount += 1;
passedTest = false;
errors[this.INPUT_TYPES.QUANTITY].push("Quantity must be greater than 0");
}
return { isOrderValid: passedTest, errors, errorCount };
}
testPrice(value, errors, isOrderValid, nextProps = null) {
const props = nextProps || this.props;
const { maxPrice, minPrice, market } = props;
const tickSize = createBigNumber(market.tickSize);
let errorCount = 0;
let passedTest = !!isOrderValid;
if (!BigNumber.isBigNumber(value))
return { isOrderValid: false, errors, errorCount };
if (value && (value.lte(minPrice) || value.gte(maxPrice))) {
errorCount += 1;
passedTest = false;
errors[this.INPUT_TYPES.PRICE].push(
`Price must be between ${minPrice} - ${maxPrice}`
);
}
// removed this validation for now, let's let augur.js handle this.
if (
value &&
value
.minus(minPrice)
.mod(tickSize)
.gt("0")
) {
errorCount += 1;
passedTest = false;
errors[this.INPUT_TYPES.PRICE].push(
`Price must be a multiple of ${tickSize}`
);
}
return { isOrderValid: passedTest, errors, errorCount };
}
orderValidation(order, nextProps = null) {
let errors = {
[this.INPUT_TYPES.QUANTITY]: [],
[this.INPUT_TYPES.PRICE]: [],
[this.INPUT_TYPES.MARKET_ORDER_SIZE]: [],
[this.TRADE_MAX_COST]: []
};
let isOrderValid = true;
let errorCount = 0;
const quantity =
order[this.INPUT_TYPES.QUANTITY] &&
createBigNumber(order[this.INPUT_TYPES.QUANTITY]);
const {
isOrderValid: quantityValid,
errors: quantityErrors,
errorCount: quantityErrorCount
} = this.testQuantity(quantity, errors, isOrderValid, nextProps);
isOrderValid = quantityValid;
errorCount += quantityErrorCount;
errors = { ...errors, ...quantityErrors };
const price =
order[this.INPUT_TYPES.PRICE] &&
createBigNumber(order[this.INPUT_TYPES.PRICE]);
const {
isOrderValid: priceValid,
errors: priceErrors,
errorCount: priceErrorCount
} = this.testPrice(price, errors, isOrderValid, nextProps);
isOrderValid = priceValid;
errorCount += priceErrorCount;
errors = { ...errors, ...priceErrors };
if (
(nextProps && nextProps.selectedOutcome.trade.potentialEthLoss) ||
this.props.selectedOutcome.trade.potentialEthLoss
) {
const { selectedOutcome } = nextProps || this.props;
const { availableFunds, gasPrice } = this.props;
const { trade } = selectedOutcome;
const { totalCost } = trade;
if (
totalCost &&
createBigNumber(totalCost.formattedValue, 10).gte(
createBigNumber(availableFunds, 10)
)
) {
isOrderValid = false;
errors = {
...errors,
[this.TRADE_MAX_COST]: ["You need more ETH to make this trade."]
};
errorCount += 1;
}
const gas =
trade.shareCost.formattedValue > 0
? this.gas.placeOrderWithSharesGasLimit
: this.gas.fillGasLimit;
const gasCost = formatGasCostToEther(
gas,
{ decimalsRounded: 4 },
gasPrice
);
const tradeTotalCost = createBigNumber(totalCost.formattedValue, 10);
if (
tradeTotalCost.gt(ZERO) &&
createBigNumber(gasCost).gt(createBigNumber(tradeTotalCost))
) {
errors = {
...errors,
[this.TRADE_MAX_COST]: [
`Est. gas cost ${gasCost} ETH, higher than order cost`
]
};
errorCount += 1;
}
}
return { isOrderValid, errors, errorCount };
}
updateTrade(updatedState, propsToUse) {
let { props } = this;
if (propsToUse) props = propsToUse;
const side = props.selectedNav;
let limitPrice = updatedState[this.INPUT_TYPES.PRICE];
let shares = updatedState[this.INPUT_TYPES.QUANTITY];
const oldShares = this.state[this.INPUT_TYPES.QUANTITY];
if (shares === null || shares === undefined || shares === "") {
shares = "0";
limitPrice = null;
}
// keep auto-filling functionality
if (
oldShares !== "" &&
(limitPrice === null || limitPrice === undefined || limitPrice === "")
) {
shares = null;
limitPrice = SCALAR ? "" : "0";
}
props.selectedOutcome.trade.updateTradeOrder(
shares,
limitPrice,
side,
null
);
}
validateForm(property, rawValue) {
const { updateState } = this.props;
// since the order changed by user action, make sure we can place orders.
// updateState('doNotCreateOrders', false)
let value = rawValue;
if (
property === this.INPUT_TYPES.QUANTITY &&
(value === "" || createBigNumber(value).lt(0))
) {
updateState(property, "");
return this.setState({
[property]: ""
});
}
if (
!(property === this.INPUT_TYPES.DO_NOT_CREATE_ORDERS) &&
!BigNumber.isBigNumber(value) &&
value !== ""
)
value = createBigNumber(value, 10);
const updatedState = {
...this.state,
[property]: value
};
const { isOrderValid, errors, errorCount } = this.orderValidation(
updatedState,
this.props
);
// update the state of the parent component to reflect new property/value
// only update the trade if there were no errors detected.
updateState(property, value);
if (errorCount === 0) {
this.updateTrade(updatedState);
}
// update the local state of this form
this.setState({
errors: {
...this.state.errors,
...errors
},
[property]: value,
isOrderValid
});
}
render() {
const {
isMobile,
market,
marketQuantity,
marketType,
nextPage,
orderEthEstimate,
orderShareEstimate,
orderType,
selectedOutcome,
maxPrice,
minPrice,
updateState
} = this.props;
const s = this.state;
const tickSize = parseFloat(market.tickSize);
const max = maxPrice.toString();
const min = minPrice.toString();
const errors = Array.from(
new Set([
...s.errors[this.INPUT_TYPES.QUANTITY],
...s.errors[this.INPUT_TYPES.PRICE],
...s.errors[this.INPUT_TYPES.MARKET_ORDER_SIZE],
...s.errors[this.TRADE_MAX_COST]
])
);
let quantityValue = s[this.INPUT_TYPES.QUANTITY];
if (BigNumber.isBigNumber(quantityValue)) {
quantityValue =
s[this.INPUT_TYPES.QUANTITY].dp() > MIN_QUANTITY.dp()
? s[this.INPUT_TYPES.QUANTITY].dp(8, 0).toFixed()
: s[this.INPUT_TYPES.QUANTITY].toNumber();
}
if (orderType === MARKET) {
return (
<ul className={Styles["TradingForm__form-body"]}>
{!isMobile &&
market.marketType === CATEGORICAL && (
<li>
<label>Outcome</label>
<div className={Styles["TradingForm__static-field"]}>
{selectedOutcome.name}
</div>
</li>
)}
<li>
<label htmlFor="tr__input--total-cost">Total Cost</label>
<input
className={classNames(FormStyles.Form__input, {
[`${Styles.error}`]: s.errors[
this.INPUT_TYPES.MARKET_ORDER_SIZE
].length
})}
id="tr__input--total-cost"
type="number"
step={tickSize}
placeholder={`${marketType === SCALAR ? tickSize : "0.0001"} ETH`}
value={
BigNumber.isBigNumber(s[this.INPUT_TYPES.MARKET_ORDER_SIZE])
? s[this.INPUT_TYPES.MARKET_ORDER_SIZE].toNumber()
: s[this.INPUT_TYPES.MARKET_ORDER_SIZE]
}
onChange={e =>
this.validateForm(
this.INPUT_TYPES.MARKET_ORDER_SIZE,
e.target.value
)
}
/>
</li>
<li>
<label>Quantity</label>
<div className={Styles["TradingForm__static-field"]}>
{marketQuantity}
</div>
</li>
{errors.length > 0 && (
<li className={Styles["TradingForm__error-message"]}>
{InputErrorIcon}{" "}
{errors.map(error => (
<p key={error}>{error}</p>
))}
</li>
)}
<li
className={
marketType === YES_NO
? Styles["TradingForm__button__yes_no--review"]
: Styles["TradingForm__button--review"]
}
>
{marketType === YES_NO && (
<label
className={TooltipStyles.TooltipHint}
data-tip
data-for="tooltip--participation-tokens"
>
{Hint}
</label>
)}
<ReactTooltip
id="tooltip--participation-tokens"
className={TooltipStyles.Tooltip}
effect="solid"
place="left"
type="light"
>
<h4>Don't think this event is going to happen?</h4>
<p>
Bet against this event occuring by selling shares of Yes (even
though you don't own them). Learn more at
docs.augur.net/#short-position
</p>
</ReactTooltip>
<button
disabled={!s.isOrderValid}
onClick={s.isOrderValid ? nextPage : undefined}
>
Review
</button>
</li>
</ul>
);
}
if (orderType === LIMIT) {
return (
<div>
<ul className={Styles["TradingForm__form-body"]}>
{!isMobile &&
market.marketType === CATEGORICAL && (
<li>
<label>Outcome</label>
<div className={Styles["TradingForm__static-field"]}>
{selectedOutcome.name}
</div>
</li>
)}
<li className={Styles["TradingForm__limit-quantity"]}>
<label htmlFor="tr__input--quantity">Quantity</label>
<input
className={classNames(FormStyles.Form__input, {
[`${Styles.error}`]: s.errors[this.INPUT_TYPES.QUANTITY]
.length
})}
id="tr__input--quantity"
type="number"
step={MIN_QUANTITY.toFixed()}
min={MIN_QUANTITY.toFixed()}
placeholder={`${
marketType === SCALAR ? tickSize : MIN_QUANTITY.toFixed()
} Shares`}
value={quantityValue}
onChange={e =>
this.validateForm(this.INPUT_TYPES.QUANTITY, e.target.value)
}
/>
</li>
<li className={Styles["TradingForm__limit-price"]}>
<label htmlFor="tr__input--limit-price">Limit Price</label>
<input
className={classNames(FormStyles.Form__input, {
[`${Styles.error}`]: s.errors[this.INPUT_TYPES.PRICE].length
})}
id="tr__input--limit-price"
type="number"
step={tickSize}
max={max}
min={min}
placeholder={`${
marketType === SCALAR ? tickSize : "0.0001"
} ETH`}
value={
BigNumber.isBigNumber(s[this.INPUT_TYPES.PRICE])
? s[this.INPUT_TYPES.PRICE].toNumber()
: s[this.INPUT_TYPES.PRICE]
}
onChange={e =>
this.validateForm(this.INPUT_TYPES.PRICE, e.target.value)
}
/>
</li>
<li className={Styles["TradingForm__do-no-create-orders"]}>
<Checkbox
id="tr__input--do-no-create-orders"
type="checkbox"
isChecked={s[this.INPUT_TYPES.DO_NOT_CREATE_ORDERS]}
value={s[this.INPUT_TYPES.DO_NOT_CREATE_ORDERS]}
onClick={e =>
updateState(
this.INPUT_TYPES.DO_NOT_CREATE_ORDERS,
!s[this.INPUT_TYPES.DO_NOT_CREATE_ORDERS]
)
}
/>
<label htmlFor="tr__input--do-no-create-orders">
Fill Orders Only
</label>
</li>
</ul>
<ul className={Styles["TradingForm__form-estimated-cost"]}>
<li>
<span>Est. Cost</span>
</li>
<li>
<span>
{orderEthEstimate &&
`${formatEther(orderEthEstimate).fullPrecision}${
formatEther(orderEthEstimate).denomination
}`}
</span>
<span>
{orderShareEstimate &&
`${formatShares(orderShareEstimate).fullPrecision}${
formatShares(orderShareEstimate).denomination
}`}
</span>
</li>
</ul>
<ul className={Styles["TradingForm__form-body"]}>
{errors.length > 0 && (
<li className={Styles["TradingForm__error-message"]}>
{InputErrorIcon}{" "}
{errors.map(error => (
<p key={error}>{error}</p>
))}
</li>
)}
<li
className={
marketType === YES_NO
? Styles["TradingForm__button__yes_no--review"]
: Styles["TradingForm__button--review"]
}
>
{marketType === YES_NO && (
<label
className={TooltipStyles.TooltipHint}
data-tip
data-for="tooltip--participation-tokens"
>
{Hint}
</label>
)}
<ReactTooltip
id="tooltip--participation-tokens"
className={TooltipStyles.Tooltip}
effect="solid"
place="left"
type="light"
>
<h4>Don't think this event is going to happen?</h4>
<p>
Bet against this event occuring by selling shares of Yes (even
though you don't own them). Learn more at
docs.augur.net/#short-position
</p>
</ReactTooltip>
<button
disabled={!s.isOrderValid}
onClick={s.isOrderValid ? nextPage : undefined}
>
Review
</button>
</li>
</ul>
</div>
);
}
}
}
export default MarketTradingForm;