add stop order ui to trade form
This commit is contained in:
parent
b548e7f61e
commit
42051e0ea5
|
@ -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'
|
||||
}`}
|
||||
/>
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -55,7 +55,7 @@ const PerpSlider = ({
|
|||
|
||||
set((s) => {
|
||||
const price =
|
||||
s.tradeForm.tradeType === 'Market'
|
||||
s.tradeForm.tradeType === 'market'
|
||||
? marketPrice
|
||||
: Number(s.tradeForm.price)
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -78,7 +78,7 @@ const SpotSlider = ({
|
|||
|
||||
set((s) => {
|
||||
const price =
|
||||
s.tradeForm.tradeType === 'Market'
|
||||
s.tradeForm.tradeType === 'market'
|
||||
? marketPrice
|
||||
: Number(s.tradeForm.price)
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue