mango-v4-ui/components/trade/AdvancedTradeForm.tsx

548 lines
17 KiB
TypeScript
Raw Normal View History

2022-09-13 23:24:26 -07:00
import {
2022-11-04 11:55:21 -07:00
HealthType,
2022-10-29 18:17:11 -07:00
PerpMarket,
PerpOrderSide,
PerpOrderType,
2022-10-10 19:16:13 -07:00
Serum3Market,
2022-09-13 23:24:26 -07:00
Serum3OrderType,
Serum3SelfTradeBehavior,
Serum3Side,
} from '@blockworks-foundation/mango-v4'
import Checkbox from '@components/forms/Checkbox'
import Button from '@components/shared/Button'
import TabButtons from '@components/shared/TabButtons'
2022-09-13 23:24:26 -07:00
import Tooltip from '@components/shared/Tooltip'
import mangoStore from '@store/mangoStore'
import Decimal from 'decimal.js'
import { useTranslation } from 'next-i18next'
2022-09-26 12:56:06 -07:00
import { useCallback, useEffect, useMemo, useState } from 'react'
import NumberFormat, {
NumberFormatValues,
SourceInfo,
} from 'react-number-format'
2022-09-13 23:24:26 -07:00
import { notify } from 'utils/notifications'
2022-09-21 18:52:25 -07:00
import SpotSlider from './SpotSlider'
2022-09-25 19:02:58 -07:00
import { calculateMarketPrice } from 'utils/tradeForm'
2022-10-28 14:46:38 -07:00
import Image from 'next/legacy/image'
2022-10-02 20:42:45 -07:00
import { QuestionMarkCircleIcon } from '@heroicons/react/20/solid'
2022-10-03 16:26:12 -07:00
import Loading from '@components/shared/Loading'
2022-10-25 21:44:09 -07:00
import TabUnderline from '@components/shared/TabUnderline'
2022-10-29 18:17:11 -07:00
import PerpSlider from './PerpSlider'
2022-11-04 11:55:21 -07:00
import HealthImpact from '@components/shared/HealthImpact'
2022-11-15 17:32:55 -08:00
import useLocalStorageState from 'hooks/useLocalStorageState'
import { TRADE_FORM_UI_KEY } from 'utils/constants'
import SpotButtonGroup from './SpotButtonGroup'
import PerpButtonGroup from './PerpButtonGroup'
2022-11-17 20:43:23 -08:00
import SolBalanceWarnings from '@components/shared/SolBalanceWarnings'
2022-11-18 11:11:06 -08:00
import useJupiterMints from 'hooks/useJupiterMints'
2022-11-19 17:40:06 -08:00
import useSelectedMarket from 'hooks/useSelectedMarket'
2022-09-13 23:24:26 -07:00
const TABS: [string, number][] = [
['Limit', 0],
['Market', 0],
]
2022-09-13 23:24:26 -07:00
const AdvancedTradeForm = () => {
2022-10-03 03:38:05 -07:00
const { t } = useTranslation(['common', 'trade'])
2022-09-13 23:24:26 -07:00
const set = mangoStore.getState().set
const tradeForm = mangoStore((s) => s.tradeForm)
2022-11-18 11:11:06 -08:00
const { mangoTokens } = useJupiterMints()
2022-11-19 17:40:06 -08:00
const { selectedMarket, price: marketPrice } = useSelectedMarket()
2022-09-13 23:24:26 -07:00
const [useMargin, setUseMargin] = useState(true)
2022-10-03 16:26:12 -07:00
const [placingOrder, setPlacingOrder] = useState(false)
2022-11-15 17:32:55 -08:00
const [tradeFormUi] = useLocalStorageState(TRADE_FORM_UI_KEY, 'Slider')
2022-09-13 23:24:26 -07:00
const baseSymbol = useMemo(() => {
2022-10-29 18:17:11 -07:00
return selectedMarket?.name.split(/-|\//)[0]
2022-09-13 23:24:26 -07:00
}, [selectedMarket])
2022-10-02 20:42:45 -07:00
const baseLogoURI = useMemo(() => {
2022-11-18 11:11:06 -08:00
if (!baseSymbol || !mangoTokens.length) return ''
2022-11-19 17:40:06 -08:00
const token =
mangoTokens.find((t) => t.symbol === baseSymbol) ||
mangoTokens.find((t) => t.symbol.includes(baseSymbol))
2022-10-02 20:42:45 -07:00
if (token) {
return token.logoURI
}
return ''
2022-11-18 11:11:06 -08:00
}, [baseSymbol, mangoTokens])
2022-10-02 20:42:45 -07:00
2022-10-29 18:17:11 -07:00
const quoteBank = useMemo(() => {
const group = mangoStore.getState().group
if (!group || !selectedMarket) return
const tokenIdx =
selectedMarket instanceof Serum3Market
? selectedMarket.quoteTokenIndex
2022-10-29 18:17:11 -07:00
: selectedMarket?.settleTokenIndex
return group?.getFirstBankByTokenIndex(tokenIdx)
2022-09-13 23:24:26 -07:00
}, [selectedMarket])
2022-10-29 18:17:11 -07:00
const quoteSymbol = useMemo(() => {
return quoteBank?.name
}, [quoteBank])
2022-10-02 20:42:45 -07:00
const quoteLogoURI = useMemo(() => {
2022-11-18 11:11:06 -08:00
if (!quoteSymbol || !mangoTokens.length) return ''
const token = mangoTokens.find((t) => t.symbol === quoteSymbol)
2022-10-02 20:42:45 -07:00
if (token) {
return token.logoURI
}
return ''
2022-11-18 11:11:06 -08:00
}, [quoteSymbol, mangoTokens])
2022-10-02 20:42:45 -07:00
2022-09-13 23:24:26 -07:00
const setTradeType = useCallback(
(tradeType: 'Limit' | 'Market') => {
set((s) => {
s.tradeForm.tradeType = tradeType
})
},
[set]
)
const handlePriceChange = useCallback(
2022-09-26 12:56:06 -07:00
(e: NumberFormatValues, info: SourceInfo) => {
if (info.source !== 'event') return
2022-09-13 23:24:26 -07:00
set((s) => {
s.tradeForm.price = e.value
2022-10-31 09:39:43 -07:00
if (s.tradeForm.baseSize && !Number.isNaN(Number(e.value))) {
2022-09-26 12:56:06 -07:00
s.tradeForm.quoteSize = (
2022-11-19 17:40:06 -08:00
(parseFloat(e.value) || 0) * parseFloat(s.tradeForm.baseSize)
2022-09-26 12:56:06 -07:00
).toString()
}
2022-09-13 23:24:26 -07:00
})
},
[set]
)
const handleBaseSizeChange = useCallback(
2022-09-26 12:56:06 -07:00
(e: NumberFormatValues, info: SourceInfo) => {
if (info.source !== 'event') return
2022-09-13 23:24:26 -07:00
set((s) => {
2022-11-20 15:35:59 -08:00
const price =
s.tradeForm.tradeType === 'Market'
? marketPrice
: parseFloat(s.tradeForm.price)
2022-11-19 17:40:06 -08:00
2022-09-13 23:24:26 -07:00
s.tradeForm.baseSize = e.value
2022-11-19 17:40:06 -08:00
if (price && e.value !== '' && !Number.isNaN(Number(e.value))) {
s.tradeForm.quoteSize = (price * parseFloat(e.value)).toString()
} else {
s.tradeForm.quoteSize = ''
2022-09-26 12:56:06 -07:00
}
})
},
2022-11-20 15:35:59 -08:00
[set, marketPrice]
2022-09-26 12:56:06 -07:00
)
const handleQuoteSizeChange = useCallback(
(e: NumberFormatValues, info: SourceInfo) => {
if (info.source !== 'event') return
set((s) => {
2022-11-20 15:35:59 -08:00
const price =
s.tradeForm.tradeType === 'Market'
? marketPrice
: parseFloat(s.tradeForm.price)
2022-09-26 12:56:06 -07:00
2022-11-19 17:40:06 -08:00
s.tradeForm.quoteSize = e.value
if (price && e.value !== '' && !Number.isNaN(Number(e.value))) {
s.tradeForm.baseSize = (parseFloat(e.value) / price).toString()
} else {
s.tradeForm.baseSize = ''
2022-09-26 12:56:06 -07:00
}
2022-09-13 23:24:26 -07:00
})
},
2022-11-20 15:35:59 -08:00
[set, marketPrice]
2022-09-13 23:24:26 -07:00
)
const handlePostOnlyChange = useCallback(
(postOnly: boolean) => {
set((s) => {
s.tradeForm.postOnly = postOnly
if (s.tradeForm.ioc === true) {
s.tradeForm.ioc = !postOnly
}
})
},
[set]
)
const handleIocChange = useCallback(
(ioc: boolean) => {
set((s) => {
s.tradeForm.ioc = ioc
if (s.tradeForm.postOnly === true) {
s.tradeForm.postOnly = !ioc
}
})
},
[set]
)
const handleSetSide = useCallback(
(side: 'buy' | 'sell') => {
set((s) => {
s.tradeForm.side = side
})
},
[set]
)
2022-09-26 12:56:06 -07:00
useEffect(() => {
const group = mangoStore.getState().group
2022-11-20 15:35:59 -08:00
if (!group || !marketPrice) return
set((s) => {
s.tradeForm.price = marketPrice.toString()
})
}, [set, marketPrice])
2022-09-26 12:56:06 -07:00
2022-09-13 23:24:26 -07:00
const handlePlaceOrder = useCallback(async () => {
const client = mangoStore.getState().client
const group = mangoStore.getState().group
const mangoAccount = mangoStore.getState().mangoAccount.current
const tradeForm = mangoStore.getState().tradeForm
const actions = mangoStore.getState().actions
2022-09-25 19:02:58 -07:00
const selectedMarket = mangoStore.getState().selectedMarket.current
2022-09-13 23:24:26 -07:00
if (!group || !mangoAccount) return
2022-10-03 16:26:12 -07:00
setPlacingOrder(true)
2022-09-13 23:24:26 -07:00
try {
2022-11-19 11:20:36 -08:00
const baseSize = new Decimal(tradeForm.baseSize).toNumber()
2022-09-25 19:02:58 -07:00
let price = new Decimal(tradeForm.price).toNumber()
if (tradeForm.tradeType === 'Market') {
const orderbook = mangoStore.getState().selectedMarket.orderbook
2022-11-17 14:08:45 -08:00
price = calculateMarketPrice(orderbook, baseSize, tradeForm.side)
2022-09-25 19:02:58 -07:00
}
2022-10-10 19:16:13 -07:00
if (selectedMarket instanceof Serum3Market) {
2022-10-29 18:17:11 -07:00
const spotOrderType = tradeForm.ioc
? Serum3OrderType.immediateOrCancel
: tradeForm.postOnly
? Serum3OrderType.postOnly
: Serum3OrderType.limit
2022-10-10 19:16:13 -07:00
const tx = await client.serum3PlaceOrder(
group,
mangoAccount,
2022-11-19 17:40:06 -08:00
selectedMarket.serumMarketExternal,
2022-10-10 19:16:13 -07:00
tradeForm.side === 'buy' ? Serum3Side.bid : Serum3Side.ask,
price,
baseSize,
Serum3SelfTradeBehavior.decrementTake,
2022-10-29 18:17:11 -07:00
spotOrderType,
2022-10-10 19:16:13 -07:00
Date.now(),
10
)
actions.reloadMangoAccount()
2022-10-31 11:26:17 -07:00
actions.fetchOpenOrders()
2022-10-10 19:16:13 -07:00
notify({
type: 'success',
title: 'Transaction successful',
txid: tx,
})
2022-10-29 18:17:11 -07:00
} else if (selectedMarket instanceof PerpMarket) {
const perpOrderType =
tradeForm.tradeType === 'Market'
? PerpOrderType.market
: tradeForm.ioc
? PerpOrderType.immediateOrCancel
: tradeForm.postOnly
? PerpOrderType.postOnly
: PerpOrderType.limit
const tx = await client.perpPlaceOrder(
group,
mangoAccount,
selectedMarket.perpMarketIndex,
tradeForm.side === 'buy' ? PerpOrderSide.bid : PerpOrderSide.ask,
price,
2022-11-20 18:29:51 -08:00
Math.abs(baseSize),
2022-10-29 18:17:11 -07:00
undefined, // maxQuoteQuantity
Date.now(),
perpOrderType,
undefined,
undefined
)
actions.reloadMangoAccount()
2022-10-31 11:26:17 -07:00
actions.fetchOpenOrders()
2022-10-29 18:17:11 -07:00
notify({
type: 'success',
title: 'Transaction successful',
txid: tx,
})
2022-10-10 19:16:13 -07:00
}
2022-09-13 23:24:26 -07:00
} catch (e: any) {
notify({
2022-09-25 19:02:58 -07:00
title: 'There was an issue.',
2022-09-13 23:24:26 -07:00
description: e.message,
2022-09-25 19:02:58 -07:00
txid: e?.txid,
2022-09-13 23:24:26 -07:00
type: 'error',
})
console.error('Place trade error:', e)
2022-10-03 16:26:12 -07:00
} finally {
setPlacingOrder(false)
2022-09-13 23:24:26 -07:00
}
2022-09-25 19:02:58 -07:00
}, [t])
2022-09-13 23:24:26 -07:00
2022-11-04 11:55:21 -07:00
const maintProjectedHealth = useMemo(() => {
const group = mangoStore.getState().group
const mangoAccount = mangoStore.getState().mangoAccount.current
if (!mangoAccount || !group || !tradeForm.baseSize) return 100
let simulatedHealthRatio: number
if (selectedMarket instanceof Serum3Market) {
simulatedHealthRatio =
tradeForm.side === 'sell'
? mangoAccount.simHealthRatioWithSerum3AskUiChanges(
group,
parseFloat(tradeForm.baseSize),
selectedMarket.serumMarketExternal,
HealthType.maint
)
: mangoAccount.simHealthRatioWithSerum3BidUiChanges(
group,
parseFloat(tradeForm.baseSize),
selectedMarket.serumMarketExternal,
HealthType.maint
)
} else {
simulatedHealthRatio =
tradeForm.side === 'sell'
? mangoAccount.simHealthRatioWithPerpAskUiChanges(
group,
selectedMarket!.perpMarketIndex,
parseFloat(tradeForm.baseSize)
)
: mangoAccount.simHealthRatioWithPerpBidUiChanges(
group,
selectedMarket!.perpMarketIndex,
parseFloat(tradeForm.baseSize)
)
}
return simulatedHealthRatio! > 100
? 100
: simulatedHealthRatio! < 0
? 0
: Math.trunc(simulatedHealthRatio!)
}, [selectedMarket, tradeForm])
2022-09-13 23:24:26 -07:00
return (
<div>
2022-09-21 22:32:48 -07:00
<div className="border-b border-th-bkg-3">
<TabButtons
activeValue={tradeForm.tradeType}
onChange={(tab: 'Limit' | 'Market') => setTradeType(tab)}
values={TABS}
2022-09-21 22:32:48 -07:00
fillWidth
/>
2022-09-13 23:24:26 -07:00
</div>
2022-11-17 20:43:23 -08:00
<div className="mt-4 px-4">
<SolBalanceWarnings />
</div>
2022-11-14 02:18:38 -08:00
<div className="mt-1 px-4 md:mt-6">
2022-10-25 21:44:09 -07:00
<TabUnderline
activeValue={tradeForm.side}
values={['buy', 'sell']}
onChange={(v) => handleSetSide(v)}
/>
2022-09-13 23:24:26 -07:00
</div>
2022-09-14 21:12:00 -07:00
<div className="mt-4 px-4">
2022-09-19 17:30:29 -07:00
{tradeForm.tradeType === 'Limit' ? (
<>
<div className="mb-2 mt-4 flex items-center justify-between">
2022-10-03 03:38:05 -07:00
<p className="text-xs text-th-fgd-3">{t('trade:limit-price')}</p>
2022-09-19 17:30:29 -07:00
</div>
2022-09-26 12:56:06 -07:00
<div className="default-transition flex items-center rounded-md border border-th-bkg-4 bg-th-bkg-1 p-2 text-xs font-bold text-th-fgd-1 md:hover:border-th-fgd-4 md:hover:bg-th-bkg-2 lg:text-base">
2022-11-22 15:49:42 -08:00
{quoteLogoURI ? (
<Image
className="rounded-full"
alt=""
width="24"
height="24"
src={quoteLogoURI}
/>
) : (
<QuestionMarkCircleIcon className="h-6 w-6 text-th-fgd-3" />
)}
2022-09-19 17:30:29 -07:00
<NumberFormat
inputMode="decimal"
thousandSeparator=","
allowNegative={false}
isNumericString={true}
decimalScale={6}
name="amountIn"
id="amountIn"
2022-11-22 15:49:42 -08:00
className="ml-2 w-full bg-transparent font-mono focus:outline-none"
2022-09-19 17:30:29 -07:00
placeholder="0.00"
value={tradeForm.price}
onValueChange={handlePriceChange}
/>
2022-09-26 12:56:06 -07:00
<div className="text-xs font-normal text-th-fgd-4">
2022-09-19 17:30:29 -07:00
{quoteSymbol}
</div>
</div>
</>
) : null}
2022-09-21 18:52:25 -07:00
<div className="my-2 flex items-center justify-between">
2022-10-03 03:38:05 -07:00
<p className="text-xs text-th-fgd-3">{t('trade:amount')}</p>
2022-09-21 18:52:25 -07:00
</div>
2022-09-26 12:56:06 -07:00
<div className="flex flex-col">
2022-10-02 20:42:45 -07:00
<div className="default-transition flex items-center rounded-md rounded-b-none border border-th-bkg-4 bg-th-bkg-1 p-2 text-xs font-bold text-th-fgd-1 md:hover:z-10 md:hover:border-th-fgd-4 md:hover:bg-th-bkg-2 lg:text-base">
{baseLogoURI ? (
<Image
className="rounded-full"
alt=""
width="24"
height="24"
src={baseLogoURI}
/>
) : (
<QuestionMarkCircleIcon className="h-6 w-6 text-th-fgd-3" />
)}
2022-09-26 12:56:06 -07:00
<NumberFormat
inputMode="decimal"
thousandSeparator=","
allowNegative={false}
isNumericString={true}
decimalScale={6}
name="amountIn"
id="amountIn"
2022-10-02 20:42:45 -07:00
className="ml-2 w-full bg-transparent font-mono focus:outline-none"
2022-09-26 12:56:06 -07:00
placeholder="0.00"
value={tradeForm.baseSize}
onValueChange={handleBaseSizeChange}
/>
<div className="text-xs font-normal text-th-fgd-4">
{baseSymbol}
</div>
</div>
2022-10-02 20:42:45 -07:00
<div className="default-transition -mt-[1px] flex items-center rounded-md rounded-t-none border border-th-bkg-4 bg-th-bkg-1 p-2 text-xs font-bold text-th-fgd-1 md:hover:border-th-fgd-4 md:hover:bg-th-bkg-2 lg:text-base">
{quoteLogoURI ? (
<Image
className="rounded-full"
alt=""
width="24"
height="24"
src={quoteLogoURI}
/>
) : (
<QuestionMarkCircleIcon className="h-6 w-6 text-th-fgd-3" />
)}
2022-09-26 12:56:06 -07:00
<NumberFormat
inputMode="decimal"
thousandSeparator=","
allowNegative={false}
isNumericString={true}
decimalScale={6}
name="amountIn"
id="amountIn"
2022-10-02 20:42:45 -07:00
className="ml-2 w-full bg-transparent font-mono focus:outline-none"
2022-09-26 12:56:06 -07:00
placeholder="0.00"
value={tradeForm.quoteSize}
onValueChange={handleQuoteSizeChange}
/>
<div className="text-xs font-normal text-th-fgd-4">
{quoteSymbol}
</div>
2022-09-21 18:52:25 -07:00
</div>
</div>
</div>
2022-11-15 17:32:55 -08:00
<div className={`${tradeFormUi === 'Slider' ? 'mt-4' : 'mt-2'} flex`}>
2022-10-29 18:17:11 -07:00
{selectedMarket instanceof Serum3Market ? (
2022-11-15 17:32:55 -08:00
tradeFormUi === 'Slider' ? (
<SpotSlider />
) : (
<SpotButtonGroup />
)
) : tradeFormUi === 'Slider' ? (
2022-10-29 18:17:11 -07:00
<PerpSlider />
2022-11-15 17:32:55 -08:00
) : (
<PerpButtonGroup />
2022-10-29 18:17:11 -07:00
)}
2022-09-13 23:24:26 -07:00
</div>
2022-09-22 22:09:38 -07:00
<div className="flex flex-wrap px-5">
2022-09-13 23:24:26 -07:00
{tradeForm.tradeType === 'Limit' ? (
2022-09-21 18:52:25 -07:00
<div className="flex">
2022-09-24 03:20:49 -07:00
<div className="mr-4 mt-4" id="trade-step-six">
2022-09-13 23:24:26 -07:00
<Tooltip
className="hidden md:block"
delay={250}
placement="left"
2022-10-03 03:38:05 -07:00
content={t('trade:tooltip-post')}
2022-09-13 23:24:26 -07:00
>
<Checkbox
checked={tradeForm.postOnly}
onChange={(e) => handlePostOnlyChange(e.target.checked)}
>
2022-10-03 03:38:05 -07:00
{t('trade:post')}
2022-09-13 23:24:26 -07:00
</Checkbox>
</Tooltip>
</div>
2022-09-24 03:20:49 -07:00
<div className="mr-4 mt-4" id="trade-step-seven">
2022-09-13 23:24:26 -07:00
<Tooltip
className="hidden md:block"
delay={250}
2022-10-03 03:38:05 -07:00
placement="left"
content={t('trade:tooltip-ioc')}
2022-09-13 23:24:26 -07:00
>
<div className="flex items-center text-xs text-th-fgd-3">
<Checkbox
checked={tradeForm.ioc}
onChange={(e) => handleIocChange(e.target.checked)}
>
IOC
</Checkbox>
</div>
</Tooltip>
</div>
</div>
) : null}
2022-10-29 18:17:11 -07:00
{selectedMarket instanceof Serum3Market ? (
<div className="mt-4" id="trade-step-eight">
<Tooltip
delay={250}
placement="left"
content={t('trade:tooltip-enable-margin')}
2022-09-13 23:24:26 -07:00
>
2022-10-29 18:17:11 -07:00
<Checkbox
checked={useMargin}
onChange={(e) => setUseMargin(e.target.checked)}
>
{t('trade:margin')}
</Checkbox>
</Tooltip>
</div>
) : null}
2022-09-13 23:24:26 -07:00
</div>
2022-09-22 22:09:38 -07:00
<div className="mt-6 flex px-4">
2022-09-13 23:24:26 -07:00
<Button
onClick={handlePlaceOrder}
2022-09-14 21:12:00 -07:00
className={`flex w-full items-center justify-center text-white ${
2022-09-13 23:24:26 -07:00
tradeForm.side === 'buy'
2022-09-14 21:12:00 -07:00
? 'bg-th-green-dark md:hover:bg-th-green'
: 'bg-th-red-dark md:hover:bg-th-red'
2022-09-13 23:24:26 -07:00
}`}
disabled={false}
2022-09-22 22:09:38 -07:00
size="large"
2022-09-13 23:24:26 -07:00
>
2022-10-03 16:26:12 -07:00
{!placingOrder ? (
<span className="capitalize">
{t('trade:place-order', { side: tradeForm.side })}
</span>
) : (
<div className="flex items-center space-x-2">
<Loading />
<span>{t('trade:placing-order')}</span>
</div>
)}
2022-09-13 23:24:26 -07:00
</Button>
</div>
2022-11-14 02:18:38 -08:00
<div className="mt-4 px-4 lg:mt-6">
<HealthImpact maintProjectedHealth={maintProjectedHealth} responsive />
2022-11-04 11:55:21 -07:00
</div>
2022-09-13 23:24:26 -07:00
</div>
)
}
export default AdvancedTradeForm