import { OracleProvider, PerpMarket, PerpOrderSide, PerpOrderType, Serum3Market, Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side, } from '@blockworks-foundation/mango-v4' import Checkbox from '@components/forms/Checkbox' import Tooltip from '@components/shared/Tooltip' import mangoStore from '@store/mangoStore' import Decimal from 'decimal.js' import { useTranslation } from 'next-i18next' import { ChangeEvent, FormEvent, useCallback, useEffect, useMemo, useState, } from 'react' import NumberFormat, { NumberFormatValues, SourceInfo, } from 'react-number-format' import * as sentry from '@sentry/nextjs' import { notify } from 'utils/notifications' import SpotSlider, { useSpotMarketMax } from './SpotSlider' import { OrderTypes, TriggerOrderTypes, calculateLimitPriceForMarketOrder, handlePlaceTriggerOrder, } from 'utils/tradeForm' import Image from 'next/legacy/image' import { QuestionMarkCircleIcon } from '@heroicons/react/20/solid' import TabUnderline from '@components/shared/TabUnderline' import PerpSlider, { usePerpMarketMax } from './PerpSlider' import useLocalStorageState from 'hooks/useLocalStorageState' import { SIZE_INPUT_UI_KEY, SOUND_SETTINGS_KEY, TRADE_CHECKBOXES_KEY, } from 'utils/constants' import SpotButtonGroup from './SpotButtonGroup' import PerpButtonGroup from './PerpButtonGroup' import SolBalanceWarnings from '@components/shared/SolBalanceWarnings' import useSelectedMarket from 'hooks/useSelectedMarket' import { floorToDecimal, formatCurrencyValue, formatNumericValue, getDecimalCount, } from 'utils/numbers' import LogoWithFallback from '@components/shared/LogoWithFallback' import ButtonGroup from '@components/forms/ButtonGroup' import TradeSummary from './TradeSummary' import useMangoAccount from 'hooks/useMangoAccount' import MaxSizeButton from './MaxSizeButton' import { INITIAL_SOUND_SETTINGS } from '@components/settings/SoundSettings' import { Howl } from 'howler' import { isMangoError } from 'types' import InlineNotification from '@components/shared/InlineNotification' import SpotMarketOrderSwapForm from './SpotMarketOrderSwapForm' import useRemainingBorrowsInPeriod from 'hooks/useRemainingBorrowsInPeriod' import dayjs from 'dayjs' import relativeTime from 'dayjs/plugin/relativeTime' import Select from '@components/forms/Select' import TriggerOrderMaxButton from './TriggerOrderMaxButton' import TradePriceDifference from '@components/shared/TradePriceDifference' import { getTokenBalance } from '@components/swap/TriggerSwapForm' import useMangoAccountAccounts from 'hooks/useMangoAccountAccounts' import useTokenPositionsFull from 'hooks/useAccountPositionsFull' import AccountSlotsFullNotification from '@components/shared/AccountSlotsFullNotification' import DepositWithdrawModal from '@components/modals/DepositWithdrawModal' import CreateAccountModal from '@components/modals/CreateAccountModal' import TradeformSubmitButton from './TradeformSubmitButton' import useIpAddress from 'hooks/useIpAddress' dayjs.extend(relativeTime) const set = mangoStore.getState().set export const successSound = new Howl({ src: ['/sounds/swap-success.mp3'], volume: 0.5, }) export const INPUT_SUFFIX_CLASSNAMES = 'absolute right-[1px] top-1/2 flex h-[calc(100%-2px)] -translate-y-1/2 items-center rounded-r-md bg-th-input-bkg px-2 text-xs font-normal text-th-fgd-4' export const INPUT_PREFIX_CLASSNAMES = 'absolute left-2 top-1/2 h-5 w-5 flex-shrink-0 -translate-y-1/2' export const DEFAULT_CHECKBOX_SETTINGS = { ioc: false, post: false, margin: true, } type TradeForm = { baseSize: number orderType: OrderTypes | TriggerOrderTypes price: string | undefined side: 'buy' | 'sell' } type FormErrors = Partial> const AdvancedTradeForm = () => { const { t } = useTranslation(['common', 'settings', 'swap', 'trade']) const { mangoAccount, mangoAccountAddress } = useMangoAccount() const { usedSerum3, totalSerum3, usedPerps, totalPerps } = useMangoAccountAccounts() const tradeForm = mangoStore((s) => s.tradeForm) const [placingOrder, setPlacingOrder] = useState(false) const [formErrors, setFormErrors] = useState({}) const [showDepositModal, setShowDepositModal] = useState(false) const [showCreateAccountModal, setShowCreateAccountModal] = useState(false) const [tradeFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider') const [savedCheckboxSettings, setSavedCheckboxSettings] = useLocalStorageState(TRADE_CHECKBOXES_KEY, DEFAULT_CHECKBOX_SETTINGS) const { ipAllowed, perpAllowed, spotAllowed, ipCountry } = useIpAddress() const [soundSettings] = useLocalStorageState( SOUND_SETTINGS_KEY, INITIAL_SOUND_SETTINGS, ) const { selectedMarket, price: oraclePrice, baseLogoURI, baseSymbol, quoteBank, quoteLogoURI, quoteSymbol, serumOrPerpMarket, marketAddress, } = useSelectedMarket() const { remainingBorrowsInPeriod, timeToNextPeriod } = useRemainingBorrowsInPeriod() const spotMax = useSpotMarketMax( mangoAccount, selectedMarket, tradeForm.side, savedCheckboxSettings.margin, ) const perpMax = usePerpMarketMax(mangoAccount, selectedMarket, tradeForm.side) const baseBank = useMemo(() => { const group = mangoStore.getState().group if (!group || !selectedMarket || selectedMarket instanceof PerpMarket) return const bank = group.getFirstBankByTokenIndex(selectedMarket.baseTokenIndex) return bank }, [selectedMarket]) // check for available account token slots const tokenPositionsFull = useTokenPositionsFull([baseBank, quoteBank]) // check for available serum account slots if serum market const serumSlotsFull = useMemo(() => { if (!selectedMarket || selectedMarket instanceof PerpMarket) return false const hasSlot = usedSerum3.find( (market) => market.marketIndex === selectedMarket.marketIndex, ) return usedSerum3.length >= totalSerum3.length && !hasSlot }, [usedSerum3, totalSerum3, selectedMarket]) // check for available perp account slots if perp market const perpSlotsFull = useMemo(() => { if (!selectedMarket || selectedMarket instanceof Serum3Market) return false const hasSlot = usedPerps.find( (market) => market.marketIndex === selectedMarket.perpMarketIndex, ) return usedPerps.length >= totalPerps.length && !hasSlot }, [usedPerps, totalPerps, selectedMarket]) const setTradeType = useCallback( (tradeType: OrderTypes | TriggerOrderTypes) => { set((s) => { s.tradeForm.tradeType = tradeType }) }, [], ) const handlePriceChange = useCallback( (e: NumberFormatValues, info: SourceInfo) => { if (info.source !== 'event') return set((s) => { s.tradeForm.price = e.value if (s.tradeForm.baseSize && !Number.isNaN(Number(e.value))) { s.tradeForm.quoteSize = ( (parseFloat(e.value) || 0) * parseFloat(s.tradeForm.baseSize) ).toString() } }) setFormErrors({}) }, [], ) const handleBaseSizeChange = useCallback( (e: NumberFormatValues, info: SourceInfo) => { if (info.source !== 'event') return set((s) => { const price = s.tradeForm.tradeType === 'Market' ? oraclePrice : Number(s.tradeForm.price) s.tradeForm.baseSize = e.value if (price && e.value !== '' && !Number.isNaN(Number(e.value))) { s.tradeForm.quoteSize = new Decimal(price).mul(e.value).toFixed() } else { s.tradeForm.quoteSize = '' } }) }, [oraclePrice], ) const handleQuoteSizeChange = useCallback( (e: NumberFormatValues, info: SourceInfo) => { if (info.source !== 'event') return set((s) => { const price = s.tradeForm.tradeType === 'Market' ? oraclePrice : Number(s.tradeForm.price) s.tradeForm.quoteSize = e.value if (price && e.value !== '' && !Number.isNaN(Number(e.value))) { s.tradeForm.baseSize = new Decimal(e.value).div(price).toFixed() } else { s.tradeForm.baseSize = '' } }) }, [oraclePrice], ) const handlePostOnlyChange = useCallback( (postOnly: boolean) => { let ioc = tradeForm.ioc if (postOnly) { ioc = !postOnly } set((s) => { s.tradeForm.postOnly = postOnly s.tradeForm.ioc = ioc }) setSavedCheckboxSettings({ ...savedCheckboxSettings, ioc: ioc, post: postOnly, }) }, [savedCheckboxSettings], ) const handleIocChange = useCallback( (ioc: boolean) => { let postOnly = tradeForm.postOnly if (ioc) { postOnly = !ioc } set((s) => { s.tradeForm.ioc = ioc s.tradeForm.postOnly = postOnly }) setSavedCheckboxSettings({ ...savedCheckboxSettings, ioc: ioc, post: postOnly, }) }, [savedCheckboxSettings], ) useEffect(() => { const { ioc, post } = savedCheckboxSettings set((s) => { s.tradeForm.ioc = ioc s.tradeForm.postOnly = post }) }, []) const handleReduceOnlyChange = useCallback((reduceOnly: boolean) => { set((s) => { s.tradeForm.reduceOnly = reduceOnly }) }, []) const handleSetSide = useCallback((side: 'buy' | 'sell') => { set((s) => { s.tradeForm.side = side }) setFormErrors({}) }, []) const handleSetMargin = useCallback( (e: ChangeEvent) => { setSavedCheckboxSettings({ ...savedCheckboxSettings, margin: e.target.checked, }) const { group } = mangoStore.getState() const { tradeType, side, price, baseSize, quoteSize } = tradeForm const tradePrice = tradeType === 'Market' ? oraclePrice : price if ( !group || !mangoAccount || !baseBank || !quoteBank || !tradePrice || !(selectedMarket instanceof Serum3Market) ) { return } const isBuySide = side === 'buy' const balanceBank = isBuySide ? quoteBank : baseBank const balance = mangoAccount.getTokenBalanceUi(balanceBank) const max = Math.max(balance, 0) const sizeToCompare = isBuySide ? quoteSize : baseSize const isSizeTooLarge = parseFloat(sizeToCompare) > max set((s) => { if (max <= 0) { s.tradeForm.baseSize = '' s.tradeForm.quoteSize = '' return } if (isSizeTooLarge) { if (isBuySide) { s.tradeForm.quoteSize = floorToDecimal(max, tickDecimals).toFixed() s.tradeForm.baseSize = floorToDecimal( max / Number(tradePrice), minOrderDecimals, ).toFixed() } else { s.tradeForm.baseSize = floorToDecimal( max, minOrderDecimals, ).toFixed() s.tradeForm.quoteSize = floorToDecimal( max * Number(tradePrice), tickDecimals, ).toFixed() } } }) }, [ baseBank, quoteBank, mangoAccount, oraclePrice, savedCheckboxSettings, selectedMarket, set, tradeForm, ], ) const tickDecimals = useMemo(() => { if (!serumOrPerpMarket) return 1 const tickSize = serumOrPerpMarket.tickSize const tickDecimals = getDecimalCount(tickSize) return tickDecimals }, [serumOrPerpMarket]) const [minOrderDecimals, minOrderSize] = useMemo(() => { if (!serumOrPerpMarket) return [1, 0.1] const minOrderSize = serumOrPerpMarket.minOrderSize const minOrderDecimals = getDecimalCount(minOrderSize) return [minOrderDecimals, minOrderSize] }, [serumOrPerpMarket]) const isMarketEnabled = useMemo(() => { const { group } = mangoStore.getState() if (!selectedMarket || !group) return false if (selectedMarket instanceof PerpMarket) { return selectedMarket.oracleLastUpdatedSlot !== 0 } else if (selectedMarket instanceof Serum3Market) { return ( baseBank?.oracleProvider == OracleProvider.Stub || (baseBank?.oracleLastUpdatedSlot !== 0 && (quoteBank?.name == 'USDC' ? true : quoteBank?.oracleLastUpdatedSlot !== 0)) ) } }, [baseBank, quoteBank, selectedMarket]) // clear form errors on base size change or new market useEffect(() => { if (Object.keys(formErrors).length) { setFormErrors({}) } }, [tradeForm.baseSize, marketAddress]) const isSanctioned = useMemo(() => { return ( !ipAllowed || (selectedMarket instanceof PerpMarket && !perpAllowed) || (selectedMarket instanceof Serum3Market && !spotAllowed) ) }, [selectedMarket, ipAllowed, perpAllowed, spotAllowed]) const hasPosition = useMemo(() => { const group = mangoStore.getState().group if (!mangoAccount || !selectedMarket || !group) return false if (selectedMarket instanceof PerpMarket) { const basePosition = mangoAccount .getPerpPosition(selectedMarket.perpMarketIndex) ?.getBasePositionUi(selectedMarket) return basePosition !== undefined && basePosition !== 0 } else if (selectedMarket instanceof Serum3Market) { const baseBank = group.getFirstBankByTokenIndex( selectedMarket.baseTokenIndex, ) const tokenPosition = mangoAccount.getTokenBalanceUi(baseBank) return tradeForm.side === 'sell' && tokenPosition !== 0 } }, [selectedMarket, ipCountry, mangoAccount, tradeForm]) const isForceReduceOnly = useMemo(() => { if (!selectedMarket) return false return selectedMarket.reduceOnly || !!(isSanctioned && hasPosition) }, [selectedMarket, isSanctioned, hasPosition]) useEffect(() => { if (isSanctioned) { set((state) => { state.tradeForm.reduceOnly = true }) setSavedCheckboxSettings({ ...savedCheckboxSettings, margin: false, }) } }, [isSanctioned]) /* * Updates the limit price on page load */ useEffect(() => { if (tradeForm.price === undefined) { const group = mangoStore.getState().group if (!group || !oraclePrice) return set((s) => { s.tradeForm.price = oraclePrice.toFixed(tickDecimals) }) } }, [oraclePrice, tickDecimals, tradeForm.price]) /* * Updates the price and the quote size when a Market order is selected */ useEffect(() => { const group = mangoStore.getState().group if ( tradeForm.tradeType === 'Market' && oraclePrice && selectedMarket && group ) { if (!isNaN(parseFloat(tradeForm.baseSize))) { const baseSize = new Decimal(tradeForm.baseSize)?.toNumber() const quoteSize = baseSize * oraclePrice set((s) => { s.tradeForm.price = oraclePrice.toFixed(tickDecimals) s.tradeForm.quoteSize = quoteSize.toFixed(tickDecimals) }) } else { set((s) => { s.tradeForm.price = oraclePrice.toFixed(tickDecimals) }) } } }, [oraclePrice, selectedMarket, tickDecimals, tradeForm]) const isTriggerOrder = useMemo(() => { return ( tradeForm.tradeType === TriggerOrderTypes.STOP_LOSS || tradeForm.tradeType === TriggerOrderTypes.TAKE_PROFIT ) }, [tradeForm.tradeType]) // default to correct side for trigger orders useEffect(() => { if (isTriggerOrder) { const balance = getTokenBalance(baseBank) set((state) => { if (balance > 0) { state.tradeForm.side = 'sell' } else { state.tradeForm.side = 'buy' } }) } }, [isTriggerOrder]) // // set default trigger price useEffect(() => { if (isTriggerOrder) { let triggerPrice = oraclePrice if (tradeForm.tradeType === TriggerOrderTypes.STOP_LOSS) { if (tradeForm.side === 'buy') { triggerPrice = oraclePrice * 1.1 } else { triggerPrice = oraclePrice * 0.9 } } else { if (tradeForm.side === 'buy') { triggerPrice = oraclePrice * 0.9 } else { triggerPrice = oraclePrice * 1.1 } } set((state) => { state.tradeForm.price = floorToDecimal( triggerPrice, tickDecimals, ).toFixed() }) } }, [isTriggerOrder, tickDecimals, tradeForm.side, tradeForm.tradeType]) const isFormValid = useCallback( (form: TradeForm) => { const { baseSize, price, orderType, side } = form const invalidFields: FormErrors = {} setFormErrors({}) const requiredFields: (keyof TradeForm)[] = ['baseSize', 'price'] const priceNumber = price ? parseFloat(price) : 0 const baseTokenBalance = getTokenBalance(baseBank) const isReducingShort = baseTokenBalance < 0 for (const key of requiredFields) { const value = form[key] as string if (!value) { invalidFields[key] = t('settings:error-required-field') } } if (orderType === TriggerOrderTypes.STOP_LOSS) { if (isReducingShort && priceNumber <= oraclePrice) { invalidFields.price = t('trade:error-trigger-above') } if (!isReducingShort && priceNumber >= oraclePrice) { invalidFields.price = t('trade:error-trigger-below') } } if (orderType === TriggerOrderTypes.TAKE_PROFIT) { if (isReducingShort && priceNumber >= oraclePrice) { invalidFields.price = t('trade:error-trigger-below') } if (!isReducingShort && priceNumber <= oraclePrice) { invalidFields.price = t('trade:error-trigger-above') } } if (side === 'buy' && !isReducingShort && isTriggerOrder) { invalidFields.baseSize = t('trade:error-no-short') } if (side === 'sell' && isReducingShort && isTriggerOrder) { invalidFields.baseSize = t('trade:error-no-long') } if (baseSize > Math.abs(baseTokenBalance) && isTriggerOrder) { invalidFields.baseSize = t('swap:insufficient-balance', { symbol: baseBank?.name, }) } if (baseSize < minOrderSize) { invalidFields.baseSize = t('trade:min-order-size-error', { minSize: formatNumericValue(minOrderSize, minOrderDecimals), symbol: baseSymbol, }) } if (selectedMarket instanceof Serum3Market && price) { const numberPrice = parseFloat(price) const priceBand = selectedMarket.oraclePriceBand if (side === 'buy') { const priceLimit = (oraclePrice / (100 * (0.98 + priceBand))) * 100 if (numberPrice < priceLimit) { invalidFields.price = t( 'trade:error-limit-price-buy-outside-band', { limit: priceLimit.toFixed(tickDecimals), }, ) } } else { const priceLimit = (oraclePrice / (100 / (0.98 + priceBand))) * 100 if (numberPrice > priceLimit) { invalidFields.price = t( 'trade:error-limit-price-sell-outside-band', { limit: priceLimit.toFixed(tickDecimals), }, ) } } } if (Object.keys(invalidFields).length) { setFormErrors(invalidFields) } return invalidFields }, [ baseBank, isTriggerOrder, minOrderDecimals, minOrderSize, oraclePrice, selectedMarket, setFormErrors, baseSymbol, t, tickDecimals, ], ) const handleStandardOrder = useCallback(async () => { const { client } = mangoStore.getState() const { group } = mangoStore.getState() const mangoAccount = mangoStore.getState().mangoAccount.current const { tradeForm } = mangoStore.getState() const { actions } = mangoStore.getState() const selectedMarket = mangoStore.getState().selectedMarket.current if (!group || !mangoAccount) return setPlacingOrder(true) try { const baseSize = Number(tradeForm.baseSize) let price = Number(tradeForm.price) if (tradeForm.tradeType === 'Market') { const orderbook = mangoStore.getState().selectedMarket.orderbook price = calculateLimitPriceForMarketOrder( orderbook, baseSize, tradeForm.side, ) } const invalidFields = isFormValid({ baseSize: baseSize, price: tradeForm.price, orderType: tradeForm.tradeType, side: tradeForm.side, }) if (Object.keys(invalidFields).length) { return } if (selectedMarket instanceof Serum3Market) { const spotOrderType = tradeForm.ioc ? Serum3OrderType.immediateOrCancel : tradeForm.postOnly && tradeForm.tradeType !== 'Market' ? Serum3OrderType.postOnly : Serum3OrderType.limit const { signature: tx } = await client.serum3PlaceOrder( group, mangoAccount, selectedMarket.serumMarketExternal, tradeForm.side === 'buy' ? Serum3Side.bid : Serum3Side.ask, price, baseSize, Serum3SelfTradeBehavior.decrementTake, spotOrderType, Date.now(), 10, ) actions.fetchOpenOrders(true) set((s) => { s.successAnimation.trade = true }) if (soundSettings['swap-success']) { successSound.play() } notify({ type: 'success', title: 'Transaction successful', txid: tx, }) } else if (selectedMarket instanceof PerpMarket) { const perpOrderType = tradeForm.tradeType === 'Market' || tradeForm.ioc ? PerpOrderType.immediateOrCancel : tradeForm.postOnly ? PerpOrderType.postOnly : PerpOrderType.limit let orderPrice = price if (tradeForm.tradeType === 'Market') { const maxSlippage = 0.025 orderPrice = price * (tradeForm.side === 'buy' ? 1 + maxSlippage : 1 - maxSlippage) } const { signature: tx } = await client.perpPlaceOrder( group, mangoAccount, selectedMarket.perpMarketIndex, tradeForm.side === 'buy' ? PerpOrderSide.bid : PerpOrderSide.ask, orderPrice, Math.abs(baseSize), undefined, // maxQuoteQuantity Date.now(), perpOrderType, selectedMarket.reduceOnly || tradeForm.reduceOnly, undefined, undefined, ) actions.fetchOpenOrders(true) set((s) => { s.successAnimation.trade = true }) if (soundSettings['swap-success']) { successSound.play() } notify({ type: 'success', title: 'Transaction successful', txid: tx, }) } } catch (e) { console.error('Place trade error:', e) sentry.captureException(e) if (!isMangoError(e)) return notify({ title: 'There was an issue.', description: e.message, txid: e?.txid, type: 'error', }) } finally { setPlacingOrder(false) } }, [isFormValid, soundSettings]) const handleTriggerOrder = useCallback(() => { const mangoAccount = mangoStore.getState().mangoAccount.current const { baseSize, price, side, tradeType } = mangoStore.getState().tradeForm const invalidFields = isFormValid({ baseSize: parseFloat(baseSize), price: price, orderType: tradeType, side, }) if (Object.keys(invalidFields).length) { return } if (!mangoAccount || !baseBank || !price) return const isReducingShort = mangoAccount.getTokenBalanceUi(baseBank) < 0 const orderType = tradeType as TriggerOrderTypes handlePlaceTriggerOrder( baseBank, quoteBank, Number(baseSize), price, orderType, isReducingShort, false, setPlacingOrder, ) }, [baseBank, quoteBank, setPlacingOrder, isFormValid]) const handleSubmit = useCallback( (e: FormEvent) => { e.preventDefault() isTriggerOrder ? handleTriggerOrder() : handleStandardOrder() }, [isTriggerOrder, handleTriggerOrder, handleStandardOrder], ) const sideNames = useMemo(() => { return selectedMarket instanceof PerpMarket ? [t('trade:long'), t('trade:short')] : [t('buy'), t('sell')] }, [selectedMarket, t]) const balanceBank = useMemo(() => { if ( !selectedMarket || selectedMarket instanceof PerpMarket || !savedCheckboxSettings.margin || isTriggerOrder ) return if (tradeForm.side === 'buy') { return quoteBank } else { return baseBank } }, [ baseBank, quoteBank, savedCheckboxSettings, selectedMarket, tradeForm.side, isTriggerOrder, ]) // check if the borrowed amount exceeds the net borrow limit in the current period const borrowExceedsLimitInPeriod = useMemo(() => { if (!mangoAccount || !balanceBank || !remainingBorrowsInPeriod) return false const size = tradeForm.side === 'buy' ? tradeForm.quoteSize : tradeForm.baseSize const balance = mangoAccount.getTokenDepositsUi(balanceBank) const remainingBalance = balance - parseFloat(size) const borrowAmount = remainingBalance < 0 ? Math.abs(remainingBalance) : 0 const borrowAmountNotional = borrowAmount * balanceBank.uiPrice return borrowAmountNotional > remainingBorrowsInPeriod }, [balanceBank, mangoAccount, remainingBorrowsInPeriod, tradeForm]) const orderTypes = useMemo(() => { const orderTypesArray = Object.values(OrderTypes) if ( !selectedMarket || selectedMarket instanceof PerpMarket || !mangoAccountAddress ) return orderTypesArray const baseBalance = floorToDecimal( getTokenBalance(baseBank), minOrderDecimals, ).toNumber() const triggerOrderTypesArray = Object.values(TriggerOrderTypes) return Math.abs(baseBalance) > 0 ? [...orderTypesArray, ...triggerOrderTypesArray] : orderTypesArray }, [baseBank, mangoAccountAddress, minOrderDecimals, selectedMarket]) const tooMuchSize = useMemo(() => { const { baseSize, quoteSize, side } = tradeForm if (!baseSize || !quoteSize) return false const size = side === 'buy' ? new Decimal(quoteSize) : new Decimal(baseSize) const decimalMax = selectedMarket instanceof Serum3Market ? new Decimal(spotMax) : new Decimal(perpMax) return size.gt(decimalMax) }, [perpMax, selectedMarket, spotMax, tradeForm]) const disabled = !serumOrPerpMarket || !isMarketEnabled || !mangoAccountAddress || !parseFloat(tradeForm.baseSize) return (
handleSetSide(v as 'buy' | 'sell')} small />

{t('trade:order-type')}

{selectedMarket instanceof PerpMarket ? ( setTradeType(tab) } values={orderTypes} /> ) : ( )}
{tradeForm.tradeType === 'Market' && selectedMarket instanceof Serum3Market ? ( ) : ( <>
handleSubmit(e)} noValidate>
{tradeForm.tradeType === 'Limit' || isTriggerOrder ? ( <>

{isTriggerOrder ? t('trade:trigger-price') : t('trade:limit-price')}

{tradeForm.price ? ( ) : null}
{quoteLogoURI ? (
) : (
)}
{quoteSymbol}
{formErrors.price ? (
) : null} ) : null}

{t('trade:size')}

{!isTriggerOrder ? ( ) : ( )}
} />
{baseSymbol}
{quoteLogoURI ? (
) : (
)}
{quoteSymbol}
{formErrors.baseSize ? (
) : null}
{selectedMarket instanceof Serum3Market ? ( tradeFormSizeUi === 'slider' ? ( ) : ( ) ) : tradeFormSizeUi === 'slider' ? ( ) : ( )}
{tradeForm.tradeType === 'Limit' ? (
handlePostOnlyChange(e.target.checked)} > {t('trade:post')}
handleIocChange(e.target.checked)} > IOC
) : null} {isTriggerOrder ? null : selectedMarket instanceof Serum3Market ? (
{t('trade:margin')}
) : (
handleReduceOnlyChange(e.target.checked) } disabled={isForceReduceOnly} > {t('trade:reduce-only')}
)}
{tradeForm.tradeType === 'Market' && selectedMarket instanceof PerpMarket ? (
) : null} {perpSlotsFull && mangoAccountAddress ? (
) : null} {serumSlotsFull && mangoAccountAddress ? (
) : null} {tokenPositionsFull && selectedMarket instanceof Serum3Market && mangoAccountAddress ? (
) : null} {borrowExceedsLimitInPeriod && remainingBorrowsInPeriod && timeToNextPeriod ? (
) : null} {isSanctioned && hasPosition ? (
) : null} {isTriggerOrder ? (
{t('swap:trigger-beta')}
  • Trigger orders on long-tail assets could be susceptible to oracle manipulation.
  • Trigger orders rely on a sufficient amount of well collateralized liquidators.
  • The slippage on existing orders could be higher/lower than what's estimated.
  • The amount of tokens used to fill your order can vary and depends on the final execution price.
  • } > {t('swap:important-info')}
    } type="info" />
    ) : null} )} {showDepositModal ? ( setShowDepositModal(false)} token={ selectedMarket instanceof Serum3Market ? tradeForm.side === 'buy' ? quoteBank?.name : baseBank?.name : 'USDC' } /> ) : null} {showCreateAccountModal ? ( setShowCreateAccountModal(false)} /> ) : null} ) } export default AdvancedTradeForm