import { HealthType, 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 { MAX_PERP_SLIPPAGE, 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 { OrderbookL2, 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' import useOpenPerpPositions from 'hooks/useOpenPerpPositions' import { isTokenInsured } from '@components/DepositForm' import UninsuredNotification from '@components/shared/UninsuredNotification' 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 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', 'account', 'settings', 'swap', 'trade', ]) const { poolIsPerpReadyForRefresh } = useOpenPerpPositions() 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 { max: 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]) const isInsured = useMemo(() => { if (selectedMarket instanceof Serum3Market) { const group = mangoStore.getState().group return isTokenInsured(baseBank, group) } else { return selectedMarket ? selectedMarket.groupInsuranceFund : true } }, [baseBank, 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 calcOrderPrice = useCallback( (price: number, orderbook: OrderbookL2) => { let orderPrice = price if (tradeForm.tradeType === 'Market') { try { if (tradeForm.side === 'sell') { const marketPrice = Math.max( oraclePrice, orderbook?.bids?.[0]?.[0] || 0, ) orderPrice = marketPrice * (1 - MAX_PERP_SLIPPAGE) } else { const marketPrice = Math.min( oraclePrice, orderbook?.asks?.[0]?.[0] || Infinity, ) orderPrice = marketPrice * (1 + MAX_PERP_SLIPPAGE) } } catch (e) { //simple fallback if something go wrong const maxSlippage = 0.025 orderPrice = price * (tradeForm.side === 'buy' ? 1 + maxSlippage : 1 - maxSlippage) } notify({ type: 'info', title: t('trade:max-slippage-price-notification', { price: `$${orderPrice.toFixed(tickDecimals)}`, }), }) } return orderPrice }, [oraclePrice, t, tickDecimals, tradeForm.side, tradeForm.tradeType], ) 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 const orderbook = mangoStore.getState().selectedMarket.orderbook if (!group || !mangoAccount) return setPlacingOrder(true) try { const baseSize = Number(tradeForm.baseSize) let price = Number(tradeForm.price) if (tradeForm.tradeType === 'Market') { 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 const orderPrice = calcOrderPrice(price, orderbook) 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, ) await poolIsPerpReadyForRefresh( () => { actions.fetchOpenOrders(true) }, () => { notify({ type: 'error', title: 'Timeout during perp refresh, please refresh data manually', }) }, ) 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, oraclePrice, soundSettings, tickDecimals]) 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 initHealth = useMemo(() => { const group = mangoStore.getState().group if (!group || !mangoAccount) return 100 return mangoAccount.getHealthRatioUi(group, HealthType.init) }, [mangoAccount]) const tooMuchSize = useMemo(() => { const { baseSize, quoteSize, side, tradeType } = tradeForm if (!baseSize || !quoteSize) return false // when init health <= 0 users may not be able to close positions via limit orders. we don't want to disable the place order button in this scenario. however we still ues this const to disable the button to restrict users from entering unwanted margin positions. if (initHealth <= 0 && tradeType === 'Limit' && mangoAccount) { if (selectedMarket instanceof Serum3Market && baseBank && quoteBank) { const balance = side === 'buy' ? mangoAccount?.getTokenBalanceUi(quoteBank) : mangoAccount?.getTokenBalanceUi(baseBank) if (balance) return false } if (selectedMarket instanceof PerpMarket) { const basePosition = mangoAccount .getPerpPosition(selectedMarket.perpMarketIndex) ?.getBasePositionUi(selectedMarket) if (basePosition !== 0 && basePosition !== undefined) { if (basePosition > 0 && side === 'sell') { return false } if (basePosition < 0 && side === 'buy') { return false } } } } // check the values in the trade form are not greater than the allowed account max 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) }, [ baseBank, initHealth, mangoAccount, perpMax, quoteBank, 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')}
)}
{initHealth <= 0 ? (
) : null} {tradeForm.tradeType === 'Market' && selectedMarket instanceof PerpMarket ? (
) : null} {!isInsured && ((selectedMarket instanceof Serum3Market && tradeForm.side === 'buy') || 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