mango-ui-v3/components/TradeForm.tsx

461 lines
13 KiB
TypeScript
Raw Normal View History

2021-04-02 11:26:21 -07:00
import { useState, useEffect, useRef } from 'react'
2021-04-18 03:34:37 -07:00
import styled from '@emotion/styled'
2021-04-02 11:26:21 -07:00
import useIpAddress from '../hooks/useIpAddress'
import {
getTokenBySymbol,
PerpMarket,
} from '@blockworks-foundation/mango-client'
2021-04-02 11:26:21 -07:00
import { notify } from '../utils/notifications'
2021-08-18 13:15:17 -07:00
import { calculateTradePrice, getDecimalCount, sleep } from '../utils'
2021-04-02 11:26:21 -07:00
import FloatingElement from './FloatingElement'
import { floorToDecimal } from '../utils/index'
import useMangoStore, { mangoClient } from '../stores/useMangoStore'
2021-04-10 12:21:02 -07:00
import Button from './Button'
import TradeType from './TradeType'
2021-04-13 16:41:04 -07:00
import Input from './Input'
import Switch from './Switch'
import { Market } from '@project-serum/serum'
import Big from 'big.js'
2021-08-17 12:37:07 -07:00
import MarketFee from './MarketFee'
2021-08-20 05:50:48 -07:00
import LeverageSlider from './LeverageSlider'
2021-04-02 11:26:21 -07:00
2021-04-18 03:34:37 -07:00
const StyledRightInput = styled(Input)`
border-left: 1px solid transparent;
`
2021-04-12 20:39:08 -07:00
export default function TradeForm() {
const set = useMangoStore((s) => s.set)
2021-08-18 11:23:12 -07:00
const { ipAllowed } = useIpAddress()
2021-04-13 22:23:50 -07:00
const connected = useMangoStore((s) => s.wallet.connected)
2021-04-12 20:39:08 -07:00
const actions = useMangoStore((s) => s.actions)
const groupConfig = useMangoStore((s) => s.selectedMangoGroup.config)
const marketConfig = useMangoStore((s) => s.selectedMarket.config)
const market = useMangoStore((s) => s.selectedMarket.current)
const { side, baseSize, quoteSize, price, tradeType } = useMangoStore(
(s) => s.tradeForm
)
2021-08-18 11:23:12 -07:00
const [postOnly, setPostOnly] = useState(false)
const [ioc, setIoc] = useState(false)
const [submitting, setSubmitting] = useState(false)
2021-04-02 11:26:21 -07:00
2021-04-29 07:38:28 -07:00
const orderBookRef = useRef(useMangoStore.getState().selectedMarket.orderBook)
const orderbook = orderBookRef.current
2021-04-02 11:26:21 -07:00
useEffect(
() =>
useMangoStore.subscribe(
2021-08-17 13:50:16 -07:00
// @ts-ignore
(orderBook) => (orderBookRef.current = orderBook),
2021-04-29 07:38:28 -07:00
(state) => state.selectedMarket.orderBook
2021-04-02 11:26:21 -07:00
),
[]
)
2021-08-18 11:23:12 -07:00
useEffect(() => {
if (tradeType === 'Market') {
set((s) => {
s.tradeForm.price = ''
})
}
}, [tradeType, set])
const setSide = (side) =>
set((s) => {
s.tradeForm.side = side
})
const setBaseSize = (baseSize) =>
set((s) => {
if (!Number.isNaN(parseFloat(baseSize))) {
s.tradeForm.baseSize = parseFloat(baseSize)
} else {
s.tradeForm.baseSize = baseSize
}
})
const setQuoteSize = (quoteSize) =>
set((s) => {
if (!Number.isNaN(parseFloat(quoteSize))) {
s.tradeForm.quoteSize = parseFloat(quoteSize)
} else {
s.tradeForm.quoteSize = quoteSize
}
})
const setPrice = (price) =>
set((s) => {
if (!Number.isNaN(parseFloat(price))) {
s.tradeForm.price = parseFloat(price)
} else {
s.tradeForm.price = price
}
})
const setTradeType = (type) =>
set((s) => {
s.tradeForm.tradeType = type
})
2021-04-29 07:38:28 -07:00
const markPriceRef = useRef(useMangoStore.getState().selectedMarket.markPrice)
2021-04-02 11:26:21 -07:00
const markPrice = markPriceRef.current
useEffect(
() =>
useMangoStore.subscribe(
(markPrice) => (markPriceRef.current = markPrice as number),
2021-04-29 07:38:28 -07:00
(state) => state.selectedMarket.markPrice
2021-04-02 11:26:21 -07:00
),
[]
)
2021-08-18 11:23:12 -07:00
let minOrderSize = '0'
if (market instanceof Market && market.minOrderSize) {
minOrderSize = market.minOrderSize.toString()
} else if (market instanceof PerpMarket) {
const baseDecimals = getTokenBySymbol(
groupConfig,
2021-06-18 15:53:24 -07:00
marketConfig.baseSymbol
).decimals
2021-07-07 08:46:25 -07:00
minOrderSize = new Big(market.baseLotSize)
.div(new Big(10).pow(baseDecimals))
.toString()
}
2021-08-18 11:23:12 -07:00
const sizeDecimalCount = getDecimalCount(minOrderSize)
2021-04-02 11:26:21 -07:00
let tickSize = 1
if (market instanceof Market) {
tickSize = market.tickSize
} else if (market instanceof PerpMarket) {
const baseDecimals = getTokenBySymbol(
groupConfig,
2021-06-18 15:53:24 -07:00
marketConfig.baseSymbol
).decimals
const quoteDecimals = getTokenBySymbol(
groupConfig,
2021-06-18 15:53:24 -07:00
groupConfig.quoteSymbol
).decimals
const nativeToUi = new Big(10).pow(baseDecimals - quoteDecimals)
const lotsToNative = new Big(market.quoteLotSize).div(
2021-07-07 08:46:25 -07:00
new Big(market.baseLotSize)
)
tickSize = lotsToNative.mul(nativeToUi).toNumber()
}
2021-04-02 11:26:21 -07:00
2021-04-15 14:36:55 -07:00
const onSetPrice = (price: number | '') => {
setPrice(price)
if (!price) return
if (baseSize) {
onSetBaseSize(baseSize)
}
}
2021-04-02 11:26:21 -07:00
2021-04-12 20:39:08 -07:00
const onSetBaseSize = (baseSize: number | '') => {
2021-06-05 09:14:34 -07:00
const { price } = useMangoStore.getState().tradeForm
2021-04-02 11:26:21 -07:00
setBaseSize(baseSize)
if (!baseSize) {
2021-04-12 20:39:08 -07:00
setQuoteSize('')
2021-04-02 11:26:21 -07:00
return
}
2021-04-12 20:39:08 -07:00
const usePrice = Number(price) || markPrice
2021-04-02 11:26:21 -07:00
if (!usePrice) {
2021-04-12 20:39:08 -07:00
setQuoteSize('')
2021-04-02 11:26:21 -07:00
return
}
const rawQuoteSize = baseSize * usePrice
const quoteSize = baseSize && floorToDecimal(rawQuoteSize, sizeDecimalCount)
2021-04-02 11:26:21 -07:00
setQuoteSize(quoteSize)
}
2021-04-12 20:39:08 -07:00
const onSetQuoteSize = (quoteSize: number | '') => {
2021-04-02 11:26:21 -07:00
setQuoteSize(quoteSize)
if (!quoteSize) {
2021-04-12 20:39:08 -07:00
setBaseSize('')
2021-04-02 11:26:21 -07:00
return
}
2021-04-12 20:39:08 -07:00
2021-04-15 14:36:55 -07:00
if (!Number(price) && tradeType === 'Limit') {
2021-04-12 20:39:08 -07:00
setBaseSize('')
2021-04-02 11:26:21 -07:00
return
}
2021-04-15 14:36:55 -07:00
const usePrice = Number(price) || markPrice
2021-04-02 11:26:21 -07:00
const rawBaseSize = quoteSize / usePrice
const baseSize = quoteSize && floorToDecimal(rawBaseSize, sizeDecimalCount)
2021-04-02 11:26:21 -07:00
setBaseSize(baseSize)
}
2021-08-18 11:23:12 -07:00
const onTradeTypeChange = (tradeType) => {
setTradeType(tradeType)
if (tradeType === 'Market') {
setIoc(true)
setPrice('')
} else {
const priceOnBook = side === 'buy' ? orderbook?.asks : orderbook?.bids
if (priceOnBook && priceOnBook.length > 0 && priceOnBook[0].length > 0) {
setPrice(priceOnBook[0][0])
}
setIoc(false)
}
}
2021-04-02 11:26:21 -07:00
const postOnChange = (checked) => {
if (checked) {
setIoc(false)
}
setPostOnly(checked)
}
const iocOnChange = (checked) => {
if (checked) {
setPostOnly(false)
}
setIoc(checked)
}
async function onSubmit() {
if (!price && tradeType === 'Limit') {
notify({
title: 'Missing price',
2021-04-02 11:26:21 -07:00
type: 'error',
})
return
} else if (!baseSize) {
notify({
title: 'Missing size',
2021-04-02 11:26:21 -07:00
type: 'error',
})
return
}
2021-06-23 08:32:33 -07:00
const mangoAccount = useMangoStore.getState().selectedMangoAccount.current
2021-04-10 14:12:15 -07:00
const mangoGroup = useMangoStore.getState().selectedMangoGroup.current
2021-08-17 13:50:16 -07:00
const { askInfo, bidInfo } = useMangoStore.getState().selectedMarket
2021-04-10 14:12:15 -07:00
const wallet = useMangoStore.getState().wallet.current
2021-06-23 08:32:33 -07:00
if (!wallet || !mangoGroup || !mangoAccount || !market) return
2021-04-02 11:26:21 -07:00
setSubmitting(true)
try {
2021-08-18 13:15:17 -07:00
const orderPrice = calculateTradePrice(
tradeType,
orderbook,
baseSize,
side,
price
)
if (!orderPrice) {
notify({
title: 'Price not available',
description: 'Please try again',
type: 'error',
})
2021-04-02 11:26:21 -07:00
}
const orderType = ioc ? 'ioc' : postOnly ? 'postOnly' : 'limit'
2021-07-07 10:27:11 -07:00
let txid
2021-06-17 17:32:45 -07:00
if (market instanceof Market) {
2021-07-07 10:27:11 -07:00
txid = await mangoClient.placeSpotOrder(
mangoGroup,
2021-06-23 08:32:33 -07:00
mangoAccount,
mangoGroup.mangoCache,
market,
wallet,
side,
orderPrice,
baseSize,
orderType
)
2021-06-17 17:32:45 -07:00
} else {
2021-07-07 10:27:11 -07:00
txid = await mangoClient.placePerpOrder(
2021-06-18 11:07:07 -07:00
mangoGroup,
2021-06-23 08:32:33 -07:00
mangoAccount,
mangoGroup.mangoCache,
2021-06-18 11:07:07 -07:00
market,
wallet,
side,
orderPrice,
baseSize,
2021-08-17 13:36:31 -07:00
orderType,
0,
2021-08-17 13:50:16 -07:00
side === 'buy' ? askInfo : bidInfo
2021-06-18 11:07:07 -07:00
)
2021-06-17 17:32:45 -07:00
}
2021-07-07 10:27:11 -07:00
notify({ title: 'Successfully placed trade', txid })
2021-04-12 20:39:08 -07:00
setPrice('')
onSetBaseSize('')
2021-04-02 11:26:21 -07:00
} catch (e) {
2021-04-11 21:17:23 -07:00
notify({
title: 'Error placing order',
2021-04-11 21:17:23 -07:00
description: e.message,
txid: e.txid,
2021-04-11 21:17:23 -07:00
type: 'error',
})
2021-04-02 11:26:21 -07:00
} finally {
2021-08-18 08:01:42 -07:00
sleep(1000).then(() => {
actions.fetchMangoAccounts()
})
2021-04-02 11:26:21 -07:00
setSubmitting(false)
}
}
const disabledTradeButton =
(!price && tradeType === 'Limit') || !baseSize || !connected || submitting
2021-04-02 11:26:21 -07:00
return (
2021-07-22 04:34:03 -07:00
<FloatingElement showConnect>
2021-08-17 12:37:07 -07:00
<div className={!connected ? 'filter blur-sm' : 'flex flex-col h-full'}>
2021-07-22 04:34:03 -07:00
<div>
<div className={`flex text-base text-th-fgd-4`}>
<button
onClick={() => setSide('buy')}
className={`flex-1 outline-none focus:outline-none`}
>
<div
className={`hover:text-th-green pb-1 transition-colors duration-500
${
2021-07-23 10:07:31 -07:00
side === 'buy'
? `text-th-green hover:text-th-green border-b-2 border-th-green`
: undefined
}`}
2021-07-22 04:34:03 -07:00
>
Buy
</div>
</button>
<button
onClick={() => setSide('sell')}
className={`flex-1 outline-none focus:outline-none`}
>
2021-07-22 04:34:03 -07:00
<div
className={`hover:text-th-red pb-1 transition-colors duration-500
${
2021-07-23 10:07:31 -07:00
side === 'sell'
? `text-th-red hover:text-th-red border-b-2 border-th-red`
: undefined
}
`}
2021-07-22 04:34:03 -07:00
>
Sell
</div>
</button>
</div>
<Input.Group className="mt-4">
<Input
type="number"
min="0"
step={tickSize}
onChange={(e) => onSetPrice(e.target.value)}
value={price}
disabled={tradeType === 'Market'}
prefix={'Price'}
suffix={groupConfig.quoteSymbol}
className="rounded-r-none"
wrapperClassName="w-3/5"
/>
<TradeType
2021-08-18 11:23:12 -07:00
onChange={onTradeTypeChange}
2021-07-22 04:34:03 -07:00
value={tradeType}
className="hover:border-th-primary flex-grow"
/>
</Input.Group>
2021-07-22 04:34:03 -07:00
<Input.Group className="mt-4">
<Input
type="number"
min="0"
step={minOrderSize}
onChange={(e) => onSetBaseSize(e.target.value)}
value={baseSize}
className="rounded-r-none"
wrapperClassName="w-3/5"
prefix={'Size'}
suffix={marketConfig.baseSymbol}
/>
<StyledRightInput
type="number"
min="0"
step={minOrderSize}
onChange={(e) => onSetQuoteSize(e.target.value)}
value={quoteSize}
className="rounded-l-none"
wrapperClassName="w-2/5"
suffix={groupConfig.quoteSymbol}
/>
</Input.Group>
2021-08-20 05:50:48 -07:00
<LeverageSlider
2021-08-18 11:23:12 -07:00
onChange={(e) => onSetBaseSize(e)}
value={baseSize ? baseSize : 0}
step={parseFloat(minOrderSize)}
disabled={false}
side={side}
2021-08-20 05:50:48 -07:00
decimalCount={sizeDecimalCount}
2021-08-18 13:15:17 -07:00
price={calculateTradePrice(
tradeType,
orderbook,
baseSize ? baseSize : 0,
side,
price
)}
2021-08-20 05:50:48 -07:00
/>
2021-07-22 04:34:03 -07:00
{tradeType !== 'Market' ? (
<div className="flex mt-2">
2021-07-22 04:34:03 -07:00
<Switch checked={postOnly} onChange={postOnChange}>
POST
2021-04-13 16:41:04 -07:00
</Switch>
2021-07-22 04:34:03 -07:00
<div className="ml-4">
<Switch checked={ioc} onChange={iocOnChange}>
IOC
</Switch>
</div>
</div>
2021-07-22 04:34:03 -07:00
) : null}
</div>
<div className={`flex pt-4`}>
2021-07-22 04:34:03 -07:00
{ipAllowed ? (
side === 'buy' ? (
<Button
disabled={disabledTradeButton}
2021-04-19 09:54:04 -07:00
onClick={onSubmit}
className={`${
2021-07-23 10:07:31 -07:00
!disabledTradeButton
2021-07-29 06:19:32 -07:00
? 'bg-th-bkg-2 border border-th-green hover:border-th-green-dark'
: 'border border-th-bkg-4'
} text-th-green hover:text-th-fgd-1 hover:bg-th-green-dark flex-grow`}
>
2021-07-29 06:19:32 -07:00
{`${baseSize > 0 ? 'Buy ' + baseSize : 'Buy '} ${
marketConfig.name.includes('PERP')
? marketConfig.name
: marketConfig.baseSymbol
}`}
</Button>
) : (
<Button
disabled={disabledTradeButton}
onClick={onSubmit}
className={`${
2021-07-23 10:07:31 -07:00
!disabledTradeButton
2021-07-29 06:19:32 -07:00
? 'bg-th-bkg-2 border border-th-red hover:border-th-red-dark'
: 'border border-th-bkg-4'
} text-th-red hover:text-th-fgd-1 hover:bg-th-red-dark flex-grow`}
>
2021-07-29 06:19:32 -07:00
{`${baseSize > 0 ? 'Sell ' + baseSize : 'Sell '} ${
marketConfig.name.includes('PERP')
? marketConfig.name
: marketConfig.baseSymbol
}`}
</Button>
)
) : (
2021-07-22 04:34:03 -07:00
<Button disabled className="flex-grow">
<span className="text-lg font-light">Country Not Allowed</span>
</Button>
)}
</div>
<div className="flex text-xs text-th-fgd-4 px-6 mt-2.5">
2021-08-17 12:37:07 -07:00
<MarketFee />
</div>
</div>
2021-04-02 11:26:21 -07:00
</FloatingElement>
)
}