add stop order ui to trade form

This commit is contained in:
saml33 2023-06-15 20:45:42 +10:00
parent b548e7f61e
commit 42051e0ea5
14 changed files with 148 additions and 64 deletions

View File

@ -1,10 +1,14 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Listbox } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import { ReactNode } from 'react'
import { TradeForm } from 'types'
interface SelectProps {
value: string | ReactNode
onChange: (x: string) => void
type Values = TradeForm['tradeType'] | ReactNode
interface SelectProps<T extends Values> {
value: T | string
onChange: (x: T) => void
children: ReactNode
className?: string
buttonClassName?: string
@ -13,7 +17,7 @@ interface SelectProps {
disabled?: boolean
}
const Select = ({
const Select = <T extends Values>({
value,
onChange,
children,
@ -22,7 +26,7 @@ const Select = ({
dropdownPanelClassName,
placeholder = 'Select',
disabled = false,
}: SelectProps) => {
}: SelectProps<T>) => {
return (
<div className={`relative ${className}`}>
<Listbox value={value} onChange={onChange} disabled={disabled}>
@ -40,7 +44,7 @@ const Select = ({
<span className="text-th-fgd-3">{placeholder}</span>
)}
<ChevronDownIcon
className={`h-5 w-5 flex-shrink-0 text-th-fgd-3 ${
className={`h-5 w-5 flex-shrink-0 text-th-fgd-2 ${
open ? 'rotate-180' : 'rotate-360'
}`}
/>

View File

@ -236,7 +236,7 @@ const Balance = ({ bank }: { bank: BankWithBalance }) => {
if (!group || !selectedMarket) return
let price: number
if (tradeForm.tradeType === 'Market') {
if (tradeForm.tradeType === 'market') {
const orderbook = mangoStore.getState().selectedMarket.orderbook
const side =
(balance > 0 && type === 'quote') || (balance < 0 && type === 'base')

View File

@ -24,7 +24,7 @@ const SuccessParticles = () => {
const tokenMint = mangoStore.getState().swap.outputBank?.mint.toString()
return mangoTokens.find((t) => t.address === tokenMint)?.logoURI
}
if (showForTrade && tradeType === 'Market') {
if (showForTrade && tradeType === 'market') {
const market = mangoStore.getState().selectedMarket.current
const side = mangoStore.getState().tradeForm.side
if (market instanceof Serum3Market) {

View File

@ -79,6 +79,8 @@ const SwapForm = () => {
const [showConfirm, setShowConfirm] = useState(false)
const [orderType, setOrderType] = useState(ORDER_TYPES[0])
const [activeTab, setActiveTab] = useState('swap')
const [limitPrice, setLimitPrice] = useState('')
const [triggerPrice, setTriggerPrice] = useState('')
const { group } = useMangoGroup()
const [swapFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider')
const { ipAllowed, ipCountry } = useIpAddress()
@ -187,6 +189,22 @@ const SwapForm = () => {
[swapMode, setAmountInFormValue]
)
const handleLimitPrice = useCallback(
(e: NumberFormatValues, info: SourceInfo) => {
if (info.source !== 'event') return
setLimitPrice(e.value)
},
[setLimitPrice]
)
const handleTriggerPrice = useCallback(
(e: NumberFormatValues, info: SourceInfo) => {
if (info.source !== 'event') return
setTriggerPrice(e.value)
},
[setTriggerPrice]
)
const handleAmountOutChange = useCallback(
(e: NumberFormatValues, info: SourceInfo) => {
if (info.source !== 'event') return
@ -411,28 +429,28 @@ const SwapForm = () => {
))}
</Select>
</div>
<div className="col-span-1">
<p className="mb-2 text-th-fgd-2">
{orderType === 'trade:limit'
? t('trade:limit-price')
: t('trade:trigger-price')}
</p>
<NumberFormat
inputMode="decimal"
thousandSeparator=","
allowNegative={false}
isNumericString={true}
decimalScale={inputBank?.mintDecimals || 6}
name="amountIn"
id="amountIn"
className="h-10 w-full rounded-lg bg-th-input-bkg p-3 text-right font-mono text-sm text-th-fgd-1 focus:border-th-fgd-4 focus:outline-none md:hover:border-th-input-border-hover md:hover:focus-visible:bg-th-bkg-3"
placeholder="0.00"
value={amountInFormValue}
onValueChange={handleAmountInChange}
isAllowed={withValueLimit}
/>
</div>
{orderType === 'trade:stop-limit' ? (
{orderType !== 'trade:limit' ? (
<div className="col-span-1">
<p className="mb-2 text-th-fgd-2">
{t('trade:trigger-price')}
</p>
<NumberFormat
inputMode="decimal"
thousandSeparator=","
allowNegative={false}
isNumericString={true}
decimalScale={outputBank?.mintDecimals || 6}
name="triggerPrice"
id="triggerPrice"
className="h-10 w-full rounded-lg bg-th-input-bkg p-3 text-right font-mono text-sm text-th-fgd-1 focus:border-th-fgd-4 focus:outline-none md:hover:border-th-input-border-hover md:hover:focus-visible:bg-th-bkg-3"
placeholder="0.00"
value={triggerPrice}
onValueChange={handleTriggerPrice}
isAllowed={withValueLimit}
/>
</div>
) : null}
{orderType !== 'trade:stop-market' ? (
<div className="col-span-1">
<p className="mb-2 text-th-fgd-2">{t('trade:limit-price')}</p>
<NumberFormat
@ -440,13 +458,13 @@ const SwapForm = () => {
thousandSeparator=","
allowNegative={false}
isNumericString={true}
decimalScale={inputBank?.mintDecimals || 6}
name="amountIn"
id="amountIn"
decimalScale={outputBank?.mintDecimals || 6}
name="limitPrice"
id="limitPrice"
className="h-10 w-full rounded-lg bg-th-input-bkg p-3 text-right font-mono text-sm text-th-fgd-1 focus:border-th-fgd-4 focus:outline-none md:hover:border-th-input-border-hover md:hover:focus-visible:bg-th-bkg-3"
placeholder="0.00"
value={amountInFormValue}
onValueChange={handleAmountInChange}
value={limitPrice}
onValueChange={handleLimitPrice}
isAllowed={withValueLimit}
/>
</div>

View File

@ -46,7 +46,6 @@ import useSelectedMarket from 'hooks/useSelectedMarket'
import { floorToDecimal, getDecimalCount } from 'utils/numbers'
import LogoWithFallback from '@components/shared/LogoWithFallback'
import useIpAddress from 'hooks/useIpAddress'
import ButtonGroup from '@components/forms/ButtonGroup'
import TradeSummary from './TradeSummary'
import useMangoAccount from 'hooks/useMangoAccount'
import MaxSizeButton from './MaxSizeButton'
@ -54,8 +53,9 @@ import { INITIAL_SOUND_SETTINGS } from '@components/settings/SoundSettings'
import { Howl } from 'howler'
import { useWallet } from '@solana/wallet-adapter-react'
import { useEnhancedWallet } from '@components/wallet/EnhancedWalletProvider'
import { isMangoError } from 'types'
import { TradeForm, isMangoError } from 'types'
import InlineNotification from '@components/shared/InlineNotification'
import Select from '@components/forms/Select'
const set = mangoStore.getState().set
@ -76,6 +76,13 @@ export const DEFAULT_CHECKBOX_SETTINGS = {
margin: false,
}
const ORDER_TYPES = [
'trade:limit',
'market',
'trade:stop-market',
'trade:stop-limit',
]
const AdvancedTradeForm = () => {
const { t } = useTranslation(['common', 'trade'])
const { mangoAccount } = useMangoAccount()
@ -101,7 +108,7 @@ const AdvancedTradeForm = () => {
serumOrPerpMarket,
} = useSelectedMarket()
const setTradeType = useCallback((tradeType: 'Limit' | 'Market') => {
const setTradeType = useCallback((tradeType: TradeForm['tradeType']) => {
set((s) => {
s.tradeForm.tradeType = tradeType
})
@ -122,12 +129,22 @@ const AdvancedTradeForm = () => {
[]
)
const handleTriggerPriceChange = useCallback(
(e: NumberFormatValues, info: SourceInfo) => {
if (info.source !== 'event') return
set((s) => {
s.tradeForm.triggerPrice = e.value
})
},
[]
)
const handleBaseSizeChange = useCallback(
(e: NumberFormatValues, info: SourceInfo) => {
if (info.source !== 'event') return
set((s) => {
const price =
s.tradeForm.tradeType === 'Market'
s.tradeForm.tradeType === 'market'
? oraclePrice
: Number(s.tradeForm.price)
@ -147,7 +164,7 @@ const AdvancedTradeForm = () => {
if (info.source !== 'event') return
set((s) => {
const price =
s.tradeForm.tradeType === 'Market'
s.tradeForm.tradeType === 'market'
? oraclePrice
: Number(s.tradeForm.price)
@ -229,7 +246,7 @@ const AdvancedTradeForm = () => {
const { group } = mangoStore.getState()
const { tradeType, side, price, baseSize, quoteSize } = tradeForm
const tradePrice = tradeType === 'Market' ? oraclePrice : price
const tradePrice = tradeType === 'market' ? oraclePrice : price
if (
!group ||
@ -342,7 +359,7 @@ const AdvancedTradeForm = () => {
useEffect(() => {
const group = mangoStore.getState().group
if (
tradeForm.tradeType === 'Market' &&
tradeForm.tradeType === 'market' &&
oraclePrice &&
selectedMarket &&
group
@ -375,7 +392,7 @@ const AdvancedTradeForm = () => {
try {
const baseSize = Number(tradeForm.baseSize)
let price = Number(tradeForm.price)
if (tradeForm.tradeType === 'Market') {
if (tradeForm.tradeType === 'market') {
const orderbook = mangoStore.getState().selectedMarket.orderbook
price = calculateLimitPriceForMarketOrder(
orderbook,
@ -387,7 +404,7 @@ const AdvancedTradeForm = () => {
if (selectedMarket instanceof Serum3Market) {
const spotOrderType = tradeForm.ioc
? Serum3OrderType.immediateOrCancel
: tradeForm.postOnly && tradeForm.tradeType !== 'Market'
: tradeForm.postOnly && tradeForm.tradeType !== 'market'
? Serum3OrderType.postOnly
: Serum3OrderType.limit
const tx = await client.serum3PlaceOrder(
@ -416,7 +433,7 @@ const AdvancedTradeForm = () => {
})
} else if (selectedMarket instanceof PerpMarket) {
const perpOrderType =
tradeForm.tradeType === 'Market'
tradeForm.tradeType === 'market'
? PerpOrderType.market
: tradeForm.ioc
? PerpOrderType.immediateOrCancel
@ -499,15 +516,55 @@ const AdvancedTradeForm = () => {
</div>
<div className="mt-1 px-2 md:mt-3 md:px-4">
<p className="mb-2 text-xs">{t('trade:order-type')}</p>
<ButtonGroup
activeValue={tradeForm.tradeType}
onChange={(tab: 'Limit' | 'Market') => setTradeType(tab)}
values={['Limit', 'Market']}
/>
<Select
value={t(tradeForm.tradeType)}
onChange={(type: TradeForm['tradeType']) => setTradeType(type)}
className="w-full"
>
{ORDER_TYPES.map((type) => (
<Select.Option key={type} value={type}>
{t(type)}
</Select.Option>
))}
</Select>
</div>
<form onSubmit={(e) => handleSubmit(e)}>
<div className="mt-3 px-3 md:px-4">
{tradeForm.tradeType === 'Limit' ? (
{tradeForm.tradeType.includes('stop') ? (
<>
<div className="mb-2 mt-3 flex items-center justify-between">
<p className="text-xs text-th-fgd-3">
{t('trade:trigger-price')}
</p>
</div>
<div className="relative">
{quoteLogoURI ? (
<div className={INPUT_PREFIX_CLASSNAMES}>
<Image alt="" width="20" height="20" src={quoteLogoURI} />
</div>
) : (
<div className={INPUT_PREFIX_CLASSNAMES}>
<QuestionMarkCircleIcon className="h-5 w-5 text-th-fgd-3" />
</div>
)}
<NumberFormat
inputMode="decimal"
thousandSeparator=","
allowNegative={false}
isNumericString={true}
decimalScale={tickDecimals}
name="triggerPrice"
id="triggerPrice"
className="flex w-full items-center rounded-md border border-th-input-border bg-th-input-bkg p-2 pl-9 font-mono text-sm font-bold text-th-fgd-1 focus:border-th-fgd-4 focus:outline-none md:hover:border-th-input-border-hover md:hover:focus-visible:border-th-fgd-4 lg:text-base"
placeholder="0.00"
value={tradeForm.triggerPrice}
onValueChange={handleTriggerPriceChange}
/>
<div className={INPUT_SUFFIX_CLASSNAMES}>{quoteSymbol}</div>
</div>
</>
) : null}
{tradeForm.tradeType.includes('limit') ? (
<>
<div className="mb-2 mt-3 flex items-center justify-between">
<p className="text-xs text-th-fgd-3">
@ -650,7 +707,7 @@ const AdvancedTradeForm = () => {
)}
</div>
<div className="flex flex-wrap px-5 md:flex-nowrap">
{tradeForm.tradeType === 'Limit' ? (
{tradeForm.tradeType === 'trade:limit' ? (
<div className="flex">
<div className="mr-3 mt-4" id="trade-step-six">
<Tooltip
@ -768,7 +825,7 @@ const AdvancedTradeForm = () => {
)}
</div>
</form>
{tradeForm.tradeType === 'Market' ? (
{tradeForm.tradeType === 'market' ? (
<div className="m-4">
<InlineNotification
type="warning"

View File

@ -139,7 +139,7 @@ const MarketCloseModal: FunctionComponent<MarketCloseModalProps> = ({
const maxSlippage = 0.025
// const perpOrderType =
// tradeForm.tradeType === 'Market'
// tradeForm.tradeType === 'market'
// ? PerpOrderType.market
// : tradeForm.ioc
// ? PerpOrderType.immediateOrCancel

View File

@ -63,7 +63,7 @@ const MaxSizeButton = ({
const set = mangoStore.getState().set
set((state) => {
if (side === 'buy') {
if (tradeType === 'Market' || !price) {
if (tradeType === 'market' || !price) {
const baseSize = floorToDecimal(max / oraclePrice, minOrderDecimals)
const quoteSize = floorToDecimal(max, tickDecimals)
state.tradeForm.baseSize = baseSize.toFixed()
@ -79,7 +79,7 @@ const MaxSizeButton = ({
}
} else {
const baseSize = floorToDecimal(max, minOrderDecimals)
if (tradeType === 'Market' || !price) {
if (tradeType === 'market' || !price) {
const quoteSize = floorToDecimal(
baseSize.mul(oraclePrice),
tickDecimals
@ -106,7 +106,7 @@ const MaxSizeButton = ({
const maxAmount = useMemo(() => {
const max = selectedMarket instanceof Serum3Market ? spotMax : perpMax || 0
const tradePrice = tradeType === 'Market' ? oraclePrice : Number(price)
const tradePrice = tradeType === 'market' ? oraclePrice : Number(price)
if (side === 'buy') {
return max / tradePrice
} else {

View File

@ -849,7 +849,10 @@ const OrderbookRow = ({
const set = mangoStore.getState().set
set((state) => {
state.tradeForm.price = formattedPrice.toFixed()
if (state.tradeForm.baseSize && state.tradeForm.tradeType === 'Limit') {
if (
state.tradeForm.baseSize &&
state.tradeForm.tradeType.includes('limit')
) {
const quoteSize = floorToDecimal(
formattedPrice.mul(new Decimal(state.tradeForm.baseSize)),
getDecimalCount(tickSize)

View File

@ -50,7 +50,7 @@ const PerpPositions = () => {
const set = mangoStore.getState().set
let price = Number(tradeForm.price)
if (tradeForm.tradeType === 'Market') {
if (tradeForm.tradeType === 'market') {
const orderbook = mangoStore.getState().selectedMarket.orderbook
price = calculateLimitPriceForMarketOrder(
orderbook,

View File

@ -55,7 +55,7 @@ const PerpSlider = ({
set((s) => {
const price =
s.tradeForm.tradeType === 'Market'
s.tradeForm.tradeType === 'market'
? marketPrice
: Number(s.tradeForm.price)

View File

@ -15,7 +15,7 @@ const Slippage = () => {
const slippage = useMemo(() => {
try {
if (tradeForm.tradeType === 'Market' && markPrice && selectedMarket) {
if (tradeForm.tradeType === 'market' && markPrice && selectedMarket) {
const orderbook = mangoStore.getState().selectedMarket.orderbook
return calculateSlippage(
orderbook,

View File

@ -78,7 +78,7 @@ const SpotSlider = ({
set((s) => {
const price =
s.tradeForm.tradeType === 'Market'
s.tradeForm.tradeType === 'market'
? marketPrice
: Number(s.tradeForm.price)

View File

@ -118,7 +118,8 @@ export const DEFAULT_TRADE_FORM: TradeForm = {
price: undefined,
baseSize: '',
quoteSize: '',
tradeType: 'Limit',
tradeType: 'trade:limit',
triggerPrice: '',
postOnly: false,
ioc: false,
reduceOnly: false,

View File

@ -358,7 +358,8 @@ export interface TradeForm {
price: string | undefined
baseSize: string
quoteSize: string
tradeType: 'Market' | 'Limit'
tradeType: 'market' | 'trade:limit' | 'trade:stop-limit' | 'trade:stop-market'
triggerPrice?: string
postOnly: boolean
ioc: boolean
reduceOnly: boolean