2022-09-13 23:24:26 -07:00
|
|
|
import {
|
2023-12-04 09:45:27 -08:00
|
|
|
OracleProvider,
|
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 Tooltip from '@components/shared/Tooltip'
|
|
|
|
import mangoStore from '@store/mangoStore'
|
|
|
|
import Decimal from 'decimal.js'
|
|
|
|
import { useTranslation } from 'next-i18next'
|
2023-03-23 20:06:16 -07:00
|
|
|
import {
|
|
|
|
ChangeEvent,
|
|
|
|
FormEvent,
|
|
|
|
useCallback,
|
|
|
|
useEffect,
|
|
|
|
useMemo,
|
|
|
|
useState,
|
|
|
|
} from 'react'
|
2022-09-26 12:56:06 -07:00
|
|
|
import NumberFormat, {
|
|
|
|
NumberFormatValues,
|
|
|
|
SourceInfo,
|
|
|
|
} from 'react-number-format'
|
2023-07-12 09:32:12 -07:00
|
|
|
import * as sentry from '@sentry/nextjs'
|
|
|
|
|
2022-09-13 23:24:26 -07:00
|
|
|
import { notify } from 'utils/notifications'
|
2023-12-03 19:35:31 -08:00
|
|
|
import SpotSlider, { useSpotMarketMax } from './SpotSlider'
|
2023-11-04 05:24:17 -07:00
|
|
|
import {
|
|
|
|
OrderTypes,
|
|
|
|
TriggerOrderTypes,
|
|
|
|
calculateLimitPriceForMarketOrder,
|
|
|
|
handlePlaceTriggerOrder,
|
|
|
|
} from 'utils/tradeForm'
|
2022-10-28 14:46:38 -07:00
|
|
|
import Image from 'next/legacy/image'
|
2023-08-15 05:39:42 -07:00
|
|
|
import { QuestionMarkCircleIcon } from '@heroicons/react/20/solid'
|
2022-10-25 21:44:09 -07:00
|
|
|
import TabUnderline from '@components/shared/TabUnderline'
|
2023-12-03 19:35:31 -08:00
|
|
|
import PerpSlider, { usePerpMarketMax } from './PerpSlider'
|
2022-11-15 17:32:55 -08:00
|
|
|
import useLocalStorageState from 'hooks/useLocalStorageState'
|
2023-04-02 21:51:36 -07:00
|
|
|
import {
|
|
|
|
SIZE_INPUT_UI_KEY,
|
|
|
|
SOUND_SETTINGS_KEY,
|
|
|
|
TRADE_CHECKBOXES_KEY,
|
|
|
|
} from 'utils/constants'
|
2022-11-15 17:32:55 -08:00
|
|
|
import SpotButtonGroup from './SpotButtonGroup'
|
|
|
|
import PerpButtonGroup from './PerpButtonGroup'
|
2022-11-17 20:43:23 -08:00
|
|
|
import SolBalanceWarnings from '@components/shared/SolBalanceWarnings'
|
2022-11-19 17:40:06 -08:00
|
|
|
import useSelectedMarket from 'hooks/useSelectedMarket'
|
2023-08-28 04:59:36 -07:00
|
|
|
import {
|
|
|
|
floorToDecimal,
|
|
|
|
formatCurrencyValue,
|
2023-12-07 19:52:18 -08:00
|
|
|
formatNumericValue,
|
2023-08-28 04:59:36 -07:00
|
|
|
getDecimalCount,
|
|
|
|
} from 'utils/numbers'
|
2022-12-09 16:33:27 -08:00
|
|
|
import LogoWithFallback from '@components/shared/LogoWithFallback'
|
2022-12-21 20:19:00 -08:00
|
|
|
import ButtonGroup from '@components/forms/ButtonGroup'
|
|
|
|
import TradeSummary from './TradeSummary'
|
|
|
|
import useMangoAccount from 'hooks/useMangoAccount'
|
2022-12-26 03:58:17 -08:00
|
|
|
import MaxSizeButton from './MaxSizeButton'
|
2023-01-02 04:19:30 -08:00
|
|
|
import { INITIAL_SOUND_SETTINGS } from '@components/settings/SoundSettings'
|
|
|
|
import { Howl } from 'howler'
|
2023-02-27 23:20:11 -08:00
|
|
|
import { isMangoError } from 'types'
|
2023-03-07 09:31:32 -08:00
|
|
|
import InlineNotification from '@components/shared/InlineNotification'
|
2023-07-16 20:41:13 -07:00
|
|
|
import SpotMarketOrderSwapForm from './SpotMarketOrderSwapForm'
|
2023-08-28 04:59:36 -07:00
|
|
|
import useRemainingBorrowsInPeriod from 'hooks/useRemainingBorrowsInPeriod'
|
|
|
|
import dayjs from 'dayjs'
|
|
|
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
2023-11-04 05:24:17 -07:00
|
|
|
import Select from '@components/forms/Select'
|
|
|
|
import TriggerOrderMaxButton from './TriggerOrderMaxButton'
|
|
|
|
import TradePriceDifference from '@components/shared/TradePriceDifference'
|
2023-11-05 03:22:05 -08:00
|
|
|
import { getTokenBalance } from '@components/swap/TriggerSwapForm'
|
2023-11-21 14:46:27 -08:00
|
|
|
import useMangoAccountAccounts from 'hooks/useMangoAccountAccounts'
|
2023-12-07 02:39:46 -08:00
|
|
|
import useTokenPositionsFull from 'hooks/useAccountPositionsFull'
|
2023-11-21 19:20:19 -08:00
|
|
|
import AccountSlotsFullNotification from '@components/shared/AccountSlotsFullNotification'
|
2023-12-01 20:11:00 -08:00
|
|
|
import DepositWithdrawModal from '@components/modals/DepositWithdrawModal'
|
|
|
|
import CreateAccountModal from '@components/modals/CreateAccountModal'
|
2023-12-02 03:43:22 -08:00
|
|
|
import TradeformSubmitButton from './TradeformSubmitButton'
|
2024-01-02 18:28:26 -08:00
|
|
|
import useIpAddress from 'hooks/useIpAddress'
|
2023-08-28 04:59:36 -07:00
|
|
|
|
|
|
|
dayjs.extend(relativeTime)
|
2022-09-13 23:24:26 -07:00
|
|
|
|
2022-12-14 12:54:25 -08:00
|
|
|
const set = mangoStore.getState().set
|
|
|
|
|
2023-06-21 17:48:15 -07:00
|
|
|
export const successSound = new Howl({
|
2023-01-02 04:19:30 -08:00
|
|
|
src: ['/sounds/swap-success.mp3'],
|
|
|
|
volume: 0.5,
|
|
|
|
})
|
|
|
|
|
2023-07-16 20:41:13 -07:00
|
|
|
export const INPUT_SUFFIX_CLASSNAMES =
|
2023-03-24 05:22:29 -07:00
|
|
|
'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'
|
|
|
|
|
2023-07-16 20:41:13 -07:00
|
|
|
export const INPUT_PREFIX_CLASSNAMES =
|
2023-03-24 05:22:29 -07:00
|
|
|
'absolute left-2 top-1/2 h-5 w-5 flex-shrink-0 -translate-y-1/2'
|
|
|
|
|
2023-05-23 04:48:26 -07:00
|
|
|
export const DEFAULT_CHECKBOX_SETTINGS = {
|
2023-04-02 21:51:36 -07:00
|
|
|
ioc: false,
|
|
|
|
post: false,
|
2023-08-25 09:46:45 -07:00
|
|
|
margin: true,
|
2023-04-02 21:51:36 -07:00
|
|
|
}
|
|
|
|
|
2023-11-05 03:22:05 -08:00
|
|
|
type TradeForm = {
|
|
|
|
baseSize: number
|
|
|
|
orderType: OrderTypes | TriggerOrderTypes
|
|
|
|
price: string | undefined
|
|
|
|
side: 'buy' | 'sell'
|
|
|
|
}
|
|
|
|
|
|
|
|
type FormErrors = Partial<Record<keyof TradeForm, string>>
|
|
|
|
|
2022-09-13 23:24:26 -07:00
|
|
|
const AdvancedTradeForm = () => {
|
2023-11-05 03:22:05 -08:00
|
|
|
const { t } = useTranslation(['common', 'settings', 'swap', 'trade'])
|
2023-12-02 03:43:22 -08:00
|
|
|
const { mangoAccount, mangoAccountAddress } = useMangoAccount()
|
2023-12-07 02:39:46 -08:00
|
|
|
const { usedSerum3, totalSerum3, usedPerps, totalPerps } =
|
|
|
|
useMangoAccountAccounts()
|
2022-09-13 23:24:26 -07:00
|
|
|
const tradeForm = mangoStore((s) => s.tradeForm)
|
2022-10-03 16:26:12 -07:00
|
|
|
const [placingOrder, setPlacingOrder] = useState(false)
|
2023-11-05 03:22:05 -08:00
|
|
|
const [formErrors, setFormErrors] = useState<FormErrors>({})
|
2023-12-01 20:11:00 -08:00
|
|
|
const [showDepositModal, setShowDepositModal] = useState(false)
|
|
|
|
const [showCreateAccountModal, setShowCreateAccountModal] = useState(false)
|
2023-01-19 12:41:11 -08:00
|
|
|
const [tradeFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider')
|
2023-04-02 21:51:36 -07:00
|
|
|
const [savedCheckboxSettings, setSavedCheckboxSettings] =
|
|
|
|
useLocalStorageState(TRADE_CHECKBOXES_KEY, DEFAULT_CHECKBOX_SETTINGS)
|
2023-10-25 06:32:54 -07:00
|
|
|
const { ipAllowed, perpAllowed, spotAllowed, ipCountry } = useIpAddress()
|
2023-01-02 04:19:30 -08:00
|
|
|
const [soundSettings] = useLocalStorageState(
|
|
|
|
SOUND_SETTINGS_KEY,
|
2023-07-21 11:47:53 -07:00
|
|
|
INITIAL_SOUND_SETTINGS,
|
2023-01-02 04:19:30 -08:00
|
|
|
)
|
2023-01-14 21:01:30 -08:00
|
|
|
const {
|
|
|
|
selectedMarket,
|
|
|
|
price: oraclePrice,
|
|
|
|
baseLogoURI,
|
|
|
|
baseSymbol,
|
2023-11-04 05:24:17 -07:00
|
|
|
quoteBank,
|
2023-01-14 21:01:30 -08:00
|
|
|
quoteLogoURI,
|
|
|
|
quoteSymbol,
|
2023-04-25 05:41:23 -07:00
|
|
|
serumOrPerpMarket,
|
2023-12-07 19:52:18 -08:00
|
|
|
marketAddress,
|
2023-01-14 21:01:30 -08:00
|
|
|
} = useSelectedMarket()
|
2023-08-28 04:59:36 -07:00
|
|
|
const { remainingBorrowsInPeriod, timeToNextPeriod } =
|
|
|
|
useRemainingBorrowsInPeriod()
|
2023-12-03 19:35:31 -08:00
|
|
|
const spotMax = useSpotMarketMax(
|
|
|
|
mangoAccount,
|
|
|
|
selectedMarket,
|
|
|
|
tradeForm.side,
|
|
|
|
savedCheckboxSettings.margin,
|
|
|
|
)
|
|
|
|
const perpMax = usePerpMarketMax(mangoAccount, selectedMarket, tradeForm.side)
|
2022-10-02 20:42:45 -07:00
|
|
|
|
2023-11-04 05:24:17 -07:00
|
|
|
const baseBank = useMemo(() => {
|
|
|
|
const group = mangoStore.getState().group
|
|
|
|
if (!group || !selectedMarket || selectedMarket instanceof PerpMarket)
|
|
|
|
return
|
|
|
|
const bank = group.getFirstBankByTokenIndex(selectedMarket.baseTokenIndex)
|
|
|
|
return bank
|
|
|
|
}, [selectedMarket])
|
|
|
|
|
2023-12-07 02:39:46 -08:00
|
|
|
// check for available account token slots
|
2023-11-21 16:30:19 -08:00
|
|
|
const tokenPositionsFull = useTokenPositionsFull([baseBank, quoteBank])
|
|
|
|
|
2023-12-07 02:39:46 -08:00
|
|
|
// 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])
|
|
|
|
|
2023-11-04 05:24:17 -07:00
|
|
|
const setTradeType = useCallback(
|
|
|
|
(tradeType: OrderTypes | TriggerOrderTypes) => {
|
|
|
|
set((s) => {
|
|
|
|
s.tradeForm.tradeType = tradeType
|
|
|
|
})
|
|
|
|
},
|
|
|
|
[],
|
|
|
|
)
|
2022-09-13 23:24:26 -07:00
|
|
|
|
|
|
|
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
|
|
|
})
|
2023-11-05 03:22:05 -08:00
|
|
|
setFormErrors({})
|
2022-09-13 23:24:26 -07:00
|
|
|
},
|
2023-07-21 11:47:53 -07:00
|
|
|
[],
|
2022-09-13 23:24:26 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
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'
|
2022-12-14 12:54:25 -08:00
|
|
|
? oraclePrice
|
|
|
|
: Number(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))) {
|
2023-03-31 16:39:07 -07:00
|
|
|
s.tradeForm.quoteSize = new Decimal(price).mul(e.value).toFixed()
|
2022-11-02 10:09:22 -07:00
|
|
|
} else {
|
|
|
|
s.tradeForm.quoteSize = ''
|
2022-09-26 12:56:06 -07:00
|
|
|
}
|
|
|
|
})
|
|
|
|
},
|
2023-07-21 11:47:53 -07:00
|
|
|
[oraclePrice],
|
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'
|
2022-12-14 12:54:25 -08:00
|
|
|
? oraclePrice
|
|
|
|
: Number(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))) {
|
2023-03-31 16:39:07 -07:00
|
|
|
s.tradeForm.baseSize = new Decimal(e.value).div(price).toFixed()
|
2022-11-02 10:09:22 -07:00
|
|
|
} else {
|
|
|
|
s.tradeForm.baseSize = ''
|
2022-09-26 12:56:06 -07:00
|
|
|
}
|
2022-09-13 23:24:26 -07:00
|
|
|
})
|
|
|
|
},
|
2023-07-21 11:47:53 -07:00
|
|
|
[oraclePrice],
|
2022-09-13 23:24:26 -07:00
|
|
|
)
|
|
|
|
|
2023-04-02 21:51:36 -07:00
|
|
|
const handlePostOnlyChange = useCallback(
|
|
|
|
(postOnly: boolean) => {
|
|
|
|
let ioc = tradeForm.ioc
|
|
|
|
if (postOnly) {
|
|
|
|
ioc = !postOnly
|
2022-12-14 12:54:25 -08:00
|
|
|
}
|
2023-04-02 21:51:36 -07:00
|
|
|
set((s) => {
|
|
|
|
s.tradeForm.postOnly = postOnly
|
|
|
|
s.tradeForm.ioc = ioc
|
|
|
|
})
|
|
|
|
setSavedCheckboxSettings({
|
|
|
|
...savedCheckboxSettings,
|
|
|
|
ioc: ioc,
|
|
|
|
post: postOnly,
|
|
|
|
})
|
|
|
|
},
|
2023-07-21 11:47:53 -07:00
|
|
|
[savedCheckboxSettings],
|
2023-04-02 21:51:36 -07:00
|
|
|
)
|
2022-09-13 23:24:26 -07:00
|
|
|
|
2023-04-02 21:51:36 -07:00
|
|
|
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,
|
|
|
|
})
|
|
|
|
},
|
2023-07-21 11:47:53 -07:00
|
|
|
[savedCheckboxSettings],
|
2023-04-02 21:51:36 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
const { ioc, post } = savedCheckboxSettings
|
2022-12-14 12:54:25 -08:00
|
|
|
set((s) => {
|
|
|
|
s.tradeForm.ioc = ioc
|
2023-04-02 21:51:36 -07:00
|
|
|
s.tradeForm.postOnly = post
|
2022-12-14 12:54:25 -08:00
|
|
|
})
|
|
|
|
}, [])
|
|
|
|
|
2023-01-16 20:00:42 -08:00
|
|
|
const handleReduceOnlyChange = useCallback((reduceOnly: boolean) => {
|
|
|
|
set((s) => {
|
|
|
|
s.tradeForm.reduceOnly = reduceOnly
|
|
|
|
})
|
|
|
|
}, [])
|
|
|
|
|
2022-12-14 12:54:25 -08:00
|
|
|
const handleSetSide = useCallback((side: 'buy' | 'sell') => {
|
|
|
|
set((s) => {
|
|
|
|
s.tradeForm.side = side
|
|
|
|
})
|
2023-11-05 03:22:05 -08:00
|
|
|
setFormErrors({})
|
2022-12-14 12:54:25 -08:00
|
|
|
}, [])
|
|
|
|
|
2023-04-02 21:51:36 -07:00
|
|
|
const handleSetMargin = useCallback(
|
|
|
|
(e: ChangeEvent<HTMLInputElement>) => {
|
|
|
|
setSavedCheckboxSettings({
|
|
|
|
...savedCheckboxSettings,
|
|
|
|
margin: e.target.checked,
|
2023-01-14 21:52:22 -08:00
|
|
|
})
|
2023-04-06 05:54:37 -07:00
|
|
|
|
|
|
|
const { group } = mangoStore.getState()
|
|
|
|
const { tradeType, side, price, baseSize, quoteSize } = tradeForm
|
|
|
|
const tradePrice = tradeType === 'Market' ? oraclePrice : price
|
|
|
|
|
|
|
|
if (
|
|
|
|
!group ||
|
|
|
|
!mangoAccount ||
|
2023-11-04 05:24:17 -07:00
|
|
|
!baseBank ||
|
|
|
|
!quoteBank ||
|
2023-04-06 05:54:37 -07:00
|
|
|
!tradePrice ||
|
|
|
|
!(selectedMarket instanceof Serum3Market)
|
|
|
|
) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const isBuySide = side === 'buy'
|
2023-11-04 05:24:17 -07:00
|
|
|
const balanceBank = isBuySide ? quoteBank : baseBank
|
|
|
|
const balance = mangoAccount.getTokenBalanceUi(balanceBank)
|
2023-04-06 05:54:37 -07:00
|
|
|
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),
|
2023-07-21 11:47:53 -07:00
|
|
|
minOrderDecimals,
|
2023-04-06 05:54:37 -07:00
|
|
|
).toFixed()
|
2023-04-02 16:52:00 -07:00
|
|
|
} else {
|
2023-04-06 05:54:37 -07:00
|
|
|
s.tradeForm.baseSize = floorToDecimal(
|
|
|
|
max,
|
2023-07-21 11:47:53 -07:00
|
|
|
minOrderDecimals,
|
2023-04-06 05:54:37 -07:00
|
|
|
).toFixed()
|
|
|
|
s.tradeForm.quoteSize = floorToDecimal(
|
|
|
|
max * Number(tradePrice),
|
2023-07-21 11:47:53 -07:00
|
|
|
tickDecimals,
|
2023-04-06 05:54:37 -07:00
|
|
|
).toFixed()
|
2023-04-02 16:52:00 -07:00
|
|
|
}
|
|
|
|
}
|
2023-04-06 05:54:37 -07:00
|
|
|
})
|
2023-04-02 21:51:36 -07:00
|
|
|
},
|
2023-04-06 05:54:37 -07:00
|
|
|
[
|
2023-11-04 05:24:17 -07:00
|
|
|
baseBank,
|
|
|
|
quoteBank,
|
2023-04-06 05:54:37 -07:00
|
|
|
mangoAccount,
|
|
|
|
oraclePrice,
|
|
|
|
savedCheckboxSettings,
|
|
|
|
selectedMarket,
|
|
|
|
set,
|
|
|
|
tradeForm,
|
2023-07-21 11:47:53 -07:00
|
|
|
],
|
2023-04-02 21:51:36 -07:00
|
|
|
)
|
2023-01-14 21:01:30 -08:00
|
|
|
|
2023-12-11 04:34:59 -08:00
|
|
|
const tickDecimals = useMemo(() => {
|
|
|
|
if (!serumOrPerpMarket) return 1
|
2023-05-23 04:48:26 -07:00
|
|
|
const tickSize = serumOrPerpMarket.tickSize
|
2023-01-04 17:09:43 -08:00
|
|
|
const tickDecimals = getDecimalCount(tickSize)
|
2023-12-11 04:34:59 -08:00
|
|
|
return tickDecimals
|
2023-05-23 04:48:26 -07:00
|
|
|
}, [serumOrPerpMarket])
|
2022-12-15 21:12:10 -08:00
|
|
|
|
2023-01-19 13:01:46 -08:00
|
|
|
const [minOrderDecimals, minOrderSize] = useMemo(() => {
|
2023-05-23 04:48:26 -07:00
|
|
|
if (!serumOrPerpMarket) return [1, 0.1]
|
|
|
|
const minOrderSize = serumOrPerpMarket.minOrderSize
|
2023-01-19 13:01:46 -08:00
|
|
|
const minOrderDecimals = getDecimalCount(minOrderSize)
|
|
|
|
return [minOrderDecimals, minOrderSize]
|
2023-05-23 04:48:26 -07:00
|
|
|
}, [serumOrPerpMarket])
|
2023-01-19 13:01:46 -08:00
|
|
|
|
2023-06-14 16:02:40 -07:00
|
|
|
const isMarketEnabled = useMemo(() => {
|
2023-11-04 05:24:17 -07:00
|
|
|
const { group } = mangoStore.getState()
|
2023-06-14 16:02:40 -07:00
|
|
|
if (!selectedMarket || !group) return false
|
|
|
|
if (selectedMarket instanceof PerpMarket) {
|
|
|
|
return selectedMarket.oracleLastUpdatedSlot !== 0
|
|
|
|
} else if (selectedMarket instanceof Serum3Market) {
|
|
|
|
return (
|
2023-12-04 09:45:27 -08:00
|
|
|
baseBank?.oracleProvider == OracleProvider.Stub ||
|
|
|
|
(baseBank?.oracleLastUpdatedSlot !== 0 &&
|
|
|
|
(quoteBank?.name == 'USDC'
|
|
|
|
? true
|
|
|
|
: quoteBank?.oracleLastUpdatedSlot !== 0))
|
2023-06-14 16:02:40 -07:00
|
|
|
)
|
|
|
|
}
|
2023-11-04 05:24:17 -07:00
|
|
|
}, [baseBank, quoteBank, selectedMarket])
|
2023-06-14 16:02:40 -07:00
|
|
|
|
2023-12-07 19:52:18 -08:00
|
|
|
// clear form errors on base size change or new market
|
|
|
|
useEffect(() => {
|
|
|
|
if (Object.keys(formErrors).length) {
|
|
|
|
setFormErrors({})
|
|
|
|
}
|
|
|
|
}, [tradeForm.baseSize, marketAddress])
|
2023-06-14 16:02:40 -07:00
|
|
|
|
2023-10-25 06:32:54 -07:00
|
|
|
const isSanctioned = useMemo(() => {
|
2024-01-03 17:15:32 -08:00
|
|
|
return (
|
|
|
|
!ipAllowed ||
|
|
|
|
(selectedMarket instanceof PerpMarket && !perpAllowed) ||
|
|
|
|
(selectedMarket instanceof Serum3Market && !spotAllowed)
|
2023-10-25 06:32:54 -07:00
|
|
|
)
|
2024-01-03 03:47:25 -08:00
|
|
|
}, [selectedMarket, ipAllowed, perpAllowed, spotAllowed])
|
2023-10-25 06:32:54 -07:00
|
|
|
|
|
|
|
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)
|
2024-01-03 17:15:32 -08:00
|
|
|
return basePosition !== undefined && basePosition !== 0
|
2023-10-25 06:32:54 -07:00
|
|
|
} else if (selectedMarket instanceof Serum3Market) {
|
|
|
|
const baseBank = group.getFirstBankByTokenIndex(
|
|
|
|
selectedMarket.baseTokenIndex,
|
|
|
|
)
|
|
|
|
const tokenPosition = mangoAccount.getTokenBalanceUi(baseBank)
|
2024-01-03 17:15:32 -08:00
|
|
|
return tradeForm.side === 'sell' && tokenPosition !== 0
|
2023-10-25 06:32:54 -07:00
|
|
|
}
|
2024-01-03 17:15:32 -08:00
|
|
|
}, [selectedMarket, ipCountry, mangoAccount, tradeForm])
|
2023-10-25 06:32:54 -07:00
|
|
|
|
|
|
|
const isForceReduceOnly = useMemo(() => {
|
|
|
|
if (!selectedMarket) return false
|
2024-01-02 18:28:26 -08:00
|
|
|
return selectedMarket.reduceOnly || !!(isSanctioned && hasPosition)
|
2024-01-03 03:47:25 -08:00
|
|
|
}, [selectedMarket, isSanctioned, hasPosition])
|
2023-10-25 06:32:54 -07:00
|
|
|
|
2024-01-03 17:15:32 -08:00
|
|
|
useEffect(() => {
|
|
|
|
if (isSanctioned) {
|
|
|
|
set((state) => {
|
|
|
|
state.tradeForm.reduceOnly = true
|
|
|
|
})
|
|
|
|
setSavedCheckboxSettings({
|
|
|
|
...savedCheckboxSettings,
|
|
|
|
margin: false,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}, [isSanctioned])
|
|
|
|
|
2022-12-14 12:54:25 -08:00
|
|
|
/*
|
|
|
|
* Updates the limit price on page load
|
|
|
|
*/
|
|
|
|
useEffect(() => {
|
|
|
|
if (tradeForm.price === undefined) {
|
|
|
|
const group = mangoStore.getState().group
|
|
|
|
if (!group || !oraclePrice) return
|
2022-09-13 23:24:26 -07:00
|
|
|
|
|
|
|
set((s) => {
|
2022-12-15 21:12:10 -08:00
|
|
|
s.tradeForm.price = oraclePrice.toFixed(tickDecimals)
|
2022-09-13 23:24:26 -07:00
|
|
|
})
|
2022-12-14 12:54:25 -08:00
|
|
|
}
|
2022-12-15 21:12:10 -08:00
|
|
|
}, [oraclePrice, tickDecimals, tradeForm.price])
|
2022-09-13 23:24:26 -07:00
|
|
|
|
2022-12-14 12:54:25 -08:00
|
|
|
/*
|
|
|
|
* Updates the price and the quote size when a Market order is selected
|
|
|
|
*/
|
2022-09-26 12:56:06 -07:00
|
|
|
useEffect(() => {
|
|
|
|
const group = mangoStore.getState().group
|
2022-12-13 19:53:06 -08:00
|
|
|
if (
|
|
|
|
tradeForm.tradeType === 'Market' &&
|
2022-12-14 12:54:25 -08:00
|
|
|
oraclePrice &&
|
2022-12-13 19:53:06 -08:00
|
|
|
selectedMarket &&
|
|
|
|
group
|
|
|
|
) {
|
2022-12-14 10:48:13 -08:00
|
|
|
if (!isNaN(parseFloat(tradeForm.baseSize))) {
|
|
|
|
const baseSize = new Decimal(tradeForm.baseSize)?.toNumber()
|
2022-12-14 12:54:25 -08:00
|
|
|
const quoteSize = baseSize * oraclePrice
|
2022-12-13 19:53:06 -08:00
|
|
|
set((s) => {
|
2022-12-15 21:12:10 -08:00
|
|
|
s.tradeForm.price = oraclePrice.toFixed(tickDecimals)
|
|
|
|
s.tradeForm.quoteSize = quoteSize.toFixed(tickDecimals)
|
2022-12-13 19:53:06 -08:00
|
|
|
})
|
|
|
|
} else {
|
|
|
|
set((s) => {
|
2022-12-15 21:12:10 -08:00
|
|
|
s.tradeForm.price = oraclePrice.toFixed(tickDecimals)
|
2022-12-13 19:53:06 -08:00
|
|
|
})
|
|
|
|
}
|
2022-11-22 16:57:54 -08:00
|
|
|
}
|
2022-12-15 21:12:10 -08:00
|
|
|
}, [oraclePrice, selectedMarket, tickDecimals, tradeForm])
|
2022-09-26 12:56:06 -07:00
|
|
|
|
2023-11-07 13:29:01 -08:00
|
|
|
const isTriggerOrder = useMemo(() => {
|
|
|
|
return (
|
|
|
|
tradeForm.tradeType === TriggerOrderTypes.STOP_LOSS ||
|
|
|
|
tradeForm.tradeType === TriggerOrderTypes.TAKE_PROFIT
|
|
|
|
)
|
|
|
|
}, [tradeForm.tradeType])
|
2023-11-04 05:24:17 -07:00
|
|
|
|
2023-11-06 17:48:32 -08:00
|
|
|
// 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])
|
|
|
|
|
2023-11-07 13:29:01 -08:00
|
|
|
// // set default trigger price
|
2023-11-04 05:24:17 -07:00
|
|
|
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])
|
|
|
|
|
2023-12-08 13:56:30 -08:00
|
|
|
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,
|
|
|
|
})
|
|
|
|
}
|
2023-12-24 03:15:31 -08:00
|
|
|
if (selectedMarket instanceof Serum3Market && price) {
|
|
|
|
const numberPrice = parseFloat(price)
|
2023-12-25 01:59:29 -08:00
|
|
|
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),
|
|
|
|
},
|
|
|
|
)
|
|
|
|
}
|
2023-12-24 03:15:31 -08:00
|
|
|
}
|
|
|
|
}
|
2023-12-08 13:56:30 -08:00
|
|
|
if (Object.keys(invalidFields).length) {
|
|
|
|
setFormErrors(invalidFields)
|
|
|
|
}
|
|
|
|
return invalidFields
|
|
|
|
},
|
|
|
|
[
|
|
|
|
baseBank,
|
|
|
|
isTriggerOrder,
|
|
|
|
minOrderDecimals,
|
|
|
|
minOrderSize,
|
|
|
|
oraclePrice,
|
2023-12-24 03:15:31 -08:00
|
|
|
selectedMarket,
|
2023-12-08 13:56:30 -08:00
|
|
|
setFormErrors,
|
|
|
|
baseSymbol,
|
|
|
|
t,
|
2023-12-25 01:59:29 -08:00
|
|
|
tickDecimals,
|
2023-12-08 13:56:30 -08:00
|
|
|
],
|
|
|
|
)
|
|
|
|
|
2023-11-04 05:24:17 -07:00
|
|
|
const handleStandardOrder = useCallback(async () => {
|
|
|
|
const { client } = mangoStore.getState()
|
|
|
|
const { group } = mangoStore.getState()
|
2022-09-13 23:24:26 -07:00
|
|
|
const mangoAccount = mangoStore.getState().mangoAccount.current
|
2023-11-04 05:24:17 -07:00
|
|
|
const { tradeForm } = mangoStore.getState()
|
|
|
|
const { actions } = mangoStore.getState()
|
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-12-14 12:54:25 -08:00
|
|
|
const baseSize = Number(tradeForm.baseSize)
|
|
|
|
let price = Number(tradeForm.price)
|
2022-09-25 19:02:58 -07:00
|
|
|
if (tradeForm.tradeType === 'Market') {
|
|
|
|
const orderbook = mangoStore.getState().selectedMarket.orderbook
|
2022-12-14 13:24:08 -08:00
|
|
|
price = calculateLimitPriceForMarketOrder(
|
|
|
|
orderbook,
|
|
|
|
baseSize,
|
2023-07-21 11:47:53 -07:00
|
|
|
tradeForm.side,
|
2022-12-14 13:24:08 -08:00
|
|
|
)
|
2022-09-25 19:02:58 -07:00
|
|
|
}
|
2023-12-08 13:59:33 -08:00
|
|
|
const invalidFields = isFormValid({
|
|
|
|
baseSize: baseSize,
|
|
|
|
price: tradeForm.price,
|
|
|
|
orderType: tradeForm.tradeType,
|
|
|
|
side: tradeForm.side,
|
|
|
|
})
|
|
|
|
if (Object.keys(invalidFields).length) {
|
|
|
|
return
|
|
|
|
}
|
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
|
2023-03-04 20:06:45 -08:00
|
|
|
: tradeForm.postOnly && tradeForm.tradeType !== 'Market'
|
2022-10-29 18:17:11 -07:00
|
|
|
? Serum3OrderType.postOnly
|
|
|
|
: Serum3OrderType.limit
|
2023-08-12 11:40:09 -07:00
|
|
|
const { signature: tx } = await client.serum3PlaceOrder(
|
2022-10-10 19:16:13 -07:00
|
|
|
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(),
|
2023-07-21 11:47:53 -07:00
|
|
|
10,
|
2022-10-10 19:16:13 -07:00
|
|
|
)
|
2023-02-21 20:29:25 -08:00
|
|
|
actions.fetchOpenOrders(true)
|
2023-01-02 04:19:30 -08:00
|
|
|
set((s) => {
|
|
|
|
s.successAnimation.trade = true
|
|
|
|
})
|
|
|
|
if (soundSettings['swap-success']) {
|
|
|
|
successSound.play()
|
|
|
|
}
|
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 =
|
2023-10-30 04:19:04 -07:00
|
|
|
tradeForm.tradeType === 'Market' || tradeForm.ioc
|
2022-10-29 18:17:11 -07:00
|
|
|
? PerpOrderType.immediateOrCancel
|
|
|
|
: tradeForm.postOnly
|
|
|
|
? PerpOrderType.postOnly
|
|
|
|
: PerpOrderType.limit
|
2023-04-08 10:24:21 -07:00
|
|
|
|
2023-10-30 04:19:04 -07:00
|
|
|
let orderPrice = price
|
|
|
|
if (tradeForm.tradeType === 'Market') {
|
|
|
|
const maxSlippage = 0.025
|
|
|
|
orderPrice =
|
|
|
|
price *
|
|
|
|
(tradeForm.side === 'buy' ? 1 + maxSlippage : 1 - maxSlippage)
|
|
|
|
}
|
|
|
|
|
2023-08-12 11:40:09 -07:00
|
|
|
const { signature: tx } = await client.perpPlaceOrder(
|
2022-10-29 18:17:11 -07:00
|
|
|
group,
|
|
|
|
mangoAccount,
|
|
|
|
selectedMarket.perpMarketIndex,
|
|
|
|
tradeForm.side === 'buy' ? PerpOrderSide.bid : PerpOrderSide.ask,
|
2023-10-30 04:19:04 -07:00
|
|
|
orderPrice,
|
2022-11-20 18:29:51 -08:00
|
|
|
Math.abs(baseSize),
|
2022-10-29 18:17:11 -07:00
|
|
|
undefined, // maxQuoteQuantity
|
|
|
|
Date.now(),
|
|
|
|
perpOrderType,
|
2023-01-16 20:00:42 -08:00
|
|
|
selectedMarket.reduceOnly || tradeForm.reduceOnly,
|
2022-10-29 18:17:11 -07:00
|
|
|
undefined,
|
2023-07-21 11:47:53 -07:00
|
|
|
undefined,
|
2022-10-29 18:17:11 -07:00
|
|
|
)
|
2023-02-21 20:29:25 -08:00
|
|
|
actions.fetchOpenOrders(true)
|
2023-01-20 03:03:31 -08:00
|
|
|
set((s) => {
|
|
|
|
s.successAnimation.trade = true
|
|
|
|
})
|
|
|
|
if (soundSettings['swap-success']) {
|
|
|
|
successSound.play()
|
|
|
|
}
|
2022-10-29 18:17:11 -07:00
|
|
|
notify({
|
|
|
|
type: 'success',
|
|
|
|
title: 'Transaction successful',
|
|
|
|
txid: tx,
|
|
|
|
})
|
2022-10-10 19:16:13 -07:00
|
|
|
}
|
2023-02-27 23:20:11 -08:00
|
|
|
} catch (e) {
|
|
|
|
console.error('Place trade error:', e)
|
2023-07-12 09:32:12 -07:00
|
|
|
sentry.captureException(e)
|
2023-02-27 23:20:11 -08:00
|
|
|
if (!isMangoError(e)) return
|
2022-09-13 23:24:26 -07:00
|
|
|
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',
|
|
|
|
})
|
2022-10-03 16:26:12 -07:00
|
|
|
} finally {
|
|
|
|
setPlacingOrder(false)
|
2022-09-13 23:24:26 -07:00
|
|
|
}
|
2023-12-08 13:56:30 -08:00
|
|
|
}, [isFormValid, soundSettings])
|
2022-09-13 23:24:26 -07:00
|
|
|
|
2023-11-04 05:24:17 -07:00
|
|
|
const handleTriggerOrder = useCallback(() => {
|
|
|
|
const mangoAccount = mangoStore.getState().mangoAccount.current
|
2023-11-05 03:22:05 -08:00
|
|
|
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
|
2023-11-04 05:24:17 -07:00
|
|
|
const isReducingShort = mangoAccount.getTokenBalanceUi(baseBank) < 0
|
2023-11-05 03:22:05 -08:00
|
|
|
const orderType = tradeType as TriggerOrderTypes
|
2023-11-04 05:24:17 -07:00
|
|
|
handlePlaceTriggerOrder(
|
|
|
|
baseBank,
|
|
|
|
quoteBank,
|
2023-11-05 03:22:05 -08:00
|
|
|
Number(baseSize),
|
|
|
|
price,
|
2023-11-04 05:24:17 -07:00
|
|
|
orderType,
|
|
|
|
isReducingShort,
|
|
|
|
false,
|
|
|
|
setPlacingOrder,
|
|
|
|
)
|
2023-12-08 13:56:30 -08:00
|
|
|
}, [baseBank, quoteBank, setPlacingOrder, isFormValid])
|
2023-11-04 05:24:17 -07:00
|
|
|
|
|
|
|
const handleSubmit = useCallback(
|
|
|
|
(e: FormEvent<HTMLFormElement>) => {
|
|
|
|
e.preventDefault()
|
|
|
|
isTriggerOrder ? handleTriggerOrder() : handleStandardOrder()
|
|
|
|
},
|
2023-12-08 13:56:30 -08:00
|
|
|
[isTriggerOrder, handleTriggerOrder, handleStandardOrder],
|
2023-11-04 05:24:17 -07:00
|
|
|
)
|
2022-09-13 23:24:26 -07:00
|
|
|
|
2023-03-11 01:35:13 -08:00
|
|
|
const sideNames = useMemo(() => {
|
|
|
|
return selectedMarket instanceof PerpMarket
|
|
|
|
? [t('trade:long'), t('trade:short')]
|
|
|
|
: [t('buy'), t('sell')]
|
2023-03-25 11:47:37 -07:00
|
|
|
}, [selectedMarket, t])
|
2023-03-11 01:35:13 -08:00
|
|
|
|
2023-08-28 04:59:36 -07:00
|
|
|
const balanceBank = useMemo(() => {
|
|
|
|
if (
|
|
|
|
!selectedMarket ||
|
|
|
|
selectedMarket instanceof PerpMarket ||
|
2023-11-05 03:22:05 -08:00
|
|
|
!savedCheckboxSettings.margin ||
|
|
|
|
isTriggerOrder
|
2023-08-28 04:59:36 -07:00
|
|
|
)
|
|
|
|
return
|
|
|
|
if (tradeForm.side === 'buy') {
|
2023-11-04 05:24:17 -07:00
|
|
|
return quoteBank
|
2023-08-28 04:59:36 -07:00
|
|
|
} else {
|
2023-11-04 05:24:17 -07:00
|
|
|
return baseBank
|
2023-08-28 04:59:36 -07:00
|
|
|
}
|
2023-11-04 05:24:17 -07:00
|
|
|
}, [
|
|
|
|
baseBank,
|
|
|
|
quoteBank,
|
|
|
|
savedCheckboxSettings,
|
|
|
|
selectedMarket,
|
|
|
|
tradeForm.side,
|
2023-11-05 03:22:05 -08:00
|
|
|
isTriggerOrder,
|
2023-11-04 05:24:17 -07:00
|
|
|
])
|
2023-08-28 04:59:36 -07:00
|
|
|
|
|
|
|
// check if the borrowed amount exceeds the net borrow limit in the current period
|
|
|
|
const borrowExceedsLimitInPeriod = useMemo(() => {
|
2023-09-09 04:32:28 -07:00
|
|
|
if (!mangoAccount || !balanceBank || !remainingBorrowsInPeriod) return false
|
2023-08-28 04:59:36 -07:00
|
|
|
const size =
|
2023-09-09 04:32:28 -07:00
|
|
|
tradeForm.side === 'buy' ? tradeForm.quoteSize : tradeForm.baseSize
|
2023-08-28 04:59:36 -07:00
|
|
|
const balance = mangoAccount.getTokenDepositsUi(balanceBank)
|
|
|
|
const remainingBalance = balance - parseFloat(size)
|
|
|
|
const borrowAmount = remainingBalance < 0 ? Math.abs(remainingBalance) : 0
|
2023-10-25 02:21:21 -07:00
|
|
|
const borrowAmountNotional = borrowAmount * balanceBank.uiPrice
|
|
|
|
return borrowAmountNotional > remainingBorrowsInPeriod
|
2023-08-28 04:59:36 -07:00
|
|
|
}, [balanceBank, mangoAccount, remainingBorrowsInPeriod, tradeForm])
|
|
|
|
|
2023-11-04 05:24:17 -07:00
|
|
|
const orderTypes = useMemo(() => {
|
|
|
|
const orderTypesArray = Object.values(OrderTypes)
|
2023-12-03 19:35:31 -08:00
|
|
|
if (
|
|
|
|
!selectedMarket ||
|
|
|
|
selectedMarket instanceof PerpMarket ||
|
|
|
|
!mangoAccountAddress
|
|
|
|
)
|
2023-11-04 05:24:17 -07:00
|
|
|
return orderTypesArray
|
2023-11-05 20:00:41 -08:00
|
|
|
const baseBalance = floorToDecimal(
|
|
|
|
getTokenBalance(baseBank),
|
|
|
|
minOrderDecimals,
|
|
|
|
).toNumber()
|
2023-11-04 05:24:17 -07:00
|
|
|
const triggerOrderTypesArray = Object.values(TriggerOrderTypes)
|
2023-11-05 20:00:41 -08:00
|
|
|
return Math.abs(baseBalance) > 0
|
|
|
|
? [...orderTypesArray, ...triggerOrderTypesArray]
|
|
|
|
: orderTypesArray
|
2023-12-03 19:35:31 -08:00
|
|
|
}, [baseBank, mangoAccountAddress, minOrderDecimals, selectedMarket])
|
2023-11-04 05:24:17 -07:00
|
|
|
|
2023-12-03 19:35:31 -08:00
|
|
|
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])
|
|
|
|
|
2023-04-25 05:41:23 -07:00
|
|
|
const disabled =
|
|
|
|
!serumOrPerpMarket ||
|
2023-08-28 04:59:36 -07:00
|
|
|
!isMarketEnabled ||
|
2023-12-05 13:58:25 -08:00
|
|
|
!mangoAccountAddress ||
|
|
|
|
!parseFloat(tradeForm.baseSize)
|
2023-04-25 05:41:23 -07:00
|
|
|
|
2022-09-13 23:24:26 -07:00
|
|
|
return (
|
|
|
|
<div>
|
2023-01-19 13:25:19 -08:00
|
|
|
<div className="mt-1.5 px-2 md:mt-0 md:px-4 md:pt-5 lg:mt-5 lg:pt-0">
|
2022-12-21 20:19:00 -08:00
|
|
|
<TabUnderline
|
|
|
|
activeValue={tradeForm.side}
|
|
|
|
values={['buy', 'sell']}
|
2023-03-11 01:35:13 -08:00
|
|
|
names={sideNames}
|
2023-02-27 23:20:11 -08:00
|
|
|
onChange={(v) => handleSetSide(v as 'buy' | 'sell')}
|
2022-12-21 20:19:00 -08:00
|
|
|
small
|
2022-09-21 22:32:48 -07:00
|
|
|
/>
|
2022-09-13 23:24:26 -07:00
|
|
|
</div>
|
2022-12-08 03:55:31 -08:00
|
|
|
<div className="px-3 md:px-4">
|
2023-01-06 01:14:54 -08:00
|
|
|
<SolBalanceWarnings className="mt-4" />
|
2022-11-17 20:43:23 -08:00
|
|
|
</div>
|
2022-12-21 20:19:00 -08:00
|
|
|
<div className="mt-1 px-2 md:mt-3 md:px-4">
|
|
|
|
<p className="mb-2 text-xs">{t('trade:order-type')}</p>
|
2023-11-04 05:24:17 -07:00
|
|
|
{selectedMarket instanceof PerpMarket ? (
|
|
|
|
<ButtonGroup
|
|
|
|
activeValue={tradeForm.tradeType}
|
|
|
|
onChange={(tab: OrderTypes | TriggerOrderTypes) =>
|
|
|
|
setTradeType(tab)
|
|
|
|
}
|
|
|
|
values={orderTypes}
|
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
<Select
|
|
|
|
value={t(tradeForm.tradeType)}
|
|
|
|
onChange={(type: OrderTypes | TriggerOrderTypes) =>
|
|
|
|
setTradeType(type)
|
|
|
|
}
|
|
|
|
>
|
|
|
|
{orderTypes.map((type) => (
|
|
|
|
<Select.Option key={type} value={type}>
|
|
|
|
<div className="flex w-full items-center justify-between">
|
|
|
|
{t(type)}
|
|
|
|
</div>
|
|
|
|
</Select.Option>
|
|
|
|
))}
|
|
|
|
</Select>
|
|
|
|
)}
|
2022-09-13 23:24:26 -07:00
|
|
|
</div>
|
2023-07-16 20:41:13 -07:00
|
|
|
{tradeForm.tradeType === 'Market' &&
|
|
|
|
selectedMarket instanceof Serum3Market ? (
|
2023-07-24 19:35:27 -07:00
|
|
|
<SpotMarketOrderSwapForm />
|
2023-07-16 20:41:13 -07:00
|
|
|
) : (
|
|
|
|
<>
|
2023-12-16 09:31:48 -08:00
|
|
|
<form onSubmit={(e) => handleSubmit(e)} noValidate>
|
2023-07-16 20:41:13 -07:00
|
|
|
<div className="mt-3 px-3 md:px-4">
|
2023-11-04 05:24:17 -07:00
|
|
|
{tradeForm.tradeType === 'Limit' || isTriggerOrder ? (
|
2023-07-16 20:41:13 -07:00
|
|
|
<>
|
|
|
|
<div className="mb-2 mt-3 flex items-center justify-between">
|
|
|
|
<p className="text-xs text-th-fgd-3">
|
2023-11-04 05:24:17 -07:00
|
|
|
{isTriggerOrder
|
|
|
|
? t('trade:trigger-price')
|
|
|
|
: t('trade:limit-price')}
|
2023-07-16 20:41:13 -07:00
|
|
|
</p>
|
2023-11-04 05:24:17 -07:00
|
|
|
{tradeForm.price ? (
|
|
|
|
<TradePriceDifference
|
|
|
|
currentPrice={oraclePrice}
|
|
|
|
newPrice={parseFloat(tradeForm.price)}
|
|
|
|
/>
|
|
|
|
) : null}
|
2023-03-23 20:06:16 -07:00
|
|
|
</div>
|
2023-07-16 20:41:13 -07:00
|
|
|
<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="price"
|
|
|
|
id="price"
|
|
|
|
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.price}
|
|
|
|
onValueChange={handlePriceChange}
|
|
|
|
/>
|
|
|
|
<div className={INPUT_SUFFIX_CLASSNAMES}>{quoteSymbol}</div>
|
2023-03-23 20:06:16 -07:00
|
|
|
</div>
|
2023-11-05 03:22:05 -08:00
|
|
|
{formErrors.price ? (
|
|
|
|
<div className="mt-1">
|
|
|
|
<InlineNotification
|
|
|
|
type="error"
|
|
|
|
desc={formErrors.price}
|
|
|
|
hideBorder
|
|
|
|
hidePadding
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
) : null}
|
2023-07-16 20:41:13 -07:00
|
|
|
</>
|
|
|
|
) : null}
|
2023-11-04 05:24:17 -07:00
|
|
|
<div className="mb-2 mt-3 flex items-center justify-between">
|
|
|
|
<p className="text-xs text-th-fgd-3">{t('trade:size')}</p>
|
|
|
|
{!isTriggerOrder ? (
|
|
|
|
<MaxSizeButton
|
|
|
|
minOrderDecimals={minOrderDecimals}
|
|
|
|
tickDecimals={tickDecimals}
|
|
|
|
useMargin={savedCheckboxSettings.margin}
|
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
<TriggerOrderMaxButton
|
|
|
|
minOrderDecimals={minOrderDecimals}
|
|
|
|
tickDecimals={tickDecimals}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</div>
|
2023-07-16 20:41:13 -07:00
|
|
|
<div className="flex flex-col">
|
|
|
|
<div className="relative">
|
|
|
|
<NumberFormat
|
|
|
|
inputMode="decimal"
|
|
|
|
thousandSeparator=","
|
|
|
|
allowNegative={false}
|
|
|
|
isNumericString={true}
|
|
|
|
decimalScale={minOrderDecimals}
|
|
|
|
name="base"
|
|
|
|
id="base"
|
|
|
|
className="relative flex w-full items-center rounded-md rounded-b-none border border-th-input-border bg-th-input-bkg p-2 pl-9 font-mono text-sm font-bold text-th-fgd-1 focus:z-10 focus:border-th-fgd-4 focus:outline-none md:hover:z-10 md:hover:border-th-input-border-hover md:hover:focus:border-th-fgd-4 lg:text-base"
|
|
|
|
placeholder="0.00"
|
|
|
|
value={tradeForm.baseSize}
|
|
|
|
onValueChange={handleBaseSizeChange}
|
|
|
|
/>
|
|
|
|
<div className={`z-10 ${INPUT_PREFIX_CLASSNAMES}`}>
|
|
|
|
<LogoWithFallback
|
|
|
|
alt=""
|
|
|
|
className="drop-shadow-md"
|
|
|
|
width={'24'}
|
|
|
|
height={'24'}
|
|
|
|
src={
|
|
|
|
baseLogoURI || `/icons/${baseSymbol?.toLowerCase()}.svg`
|
|
|
|
}
|
|
|
|
fallback={
|
|
|
|
<QuestionMarkCircleIcon
|
|
|
|
className={`h-5 w-5 text-th-fgd-3`}
|
|
|
|
/>
|
|
|
|
}
|
2023-03-24 05:22:29 -07:00
|
|
|
/>
|
2023-07-16 20:41:13 -07:00
|
|
|
</div>
|
|
|
|
<div className={`z-10 ${INPUT_SUFFIX_CLASSNAMES}`}>
|
|
|
|
{baseSymbol}
|
|
|
|
</div>
|
|
|
|
</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="quote"
|
|
|
|
id="quote"
|
|
|
|
className="-mt-[1px] flex w-full items-center rounded-md rounded-t-none 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:border-th-fgd-4 lg:text-base"
|
|
|
|
placeholder="0.00"
|
|
|
|
value={tradeForm.quoteSize}
|
|
|
|
onValueChange={handleQuoteSizeChange}
|
|
|
|
/>
|
|
|
|
<div className={INPUT_SUFFIX_CLASSNAMES}>{quoteSymbol}</div>
|
|
|
|
</div>
|
2023-11-05 03:22:05 -08:00
|
|
|
{formErrors.baseSize ? (
|
2023-07-16 20:41:13 -07:00
|
|
|
<div className="mt-1">
|
|
|
|
<InlineNotification
|
|
|
|
type="error"
|
2023-11-05 03:22:05 -08:00
|
|
|
desc={formErrors.baseSize}
|
2023-07-16 20:41:13 -07:00
|
|
|
hideBorder
|
|
|
|
hidePadding
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
) : null}
|
2023-03-23 20:06:16 -07:00
|
|
|
</div>
|
2022-09-19 17:30:29 -07:00
|
|
|
</div>
|
2023-07-16 20:41:13 -07:00
|
|
|
<div className="mt-2 flex">
|
|
|
|
{selectedMarket instanceof Serum3Market ? (
|
|
|
|
tradeFormSizeUi === 'slider' ? (
|
|
|
|
<SpotSlider
|
|
|
|
minOrderDecimals={minOrderDecimals}
|
|
|
|
tickDecimals={tickDecimals}
|
2023-12-11 04:34:59 -08:00
|
|
|
step={spotMax / 100}
|
2023-07-16 20:41:13 -07:00
|
|
|
useMargin={savedCheckboxSettings.margin}
|
2023-11-21 15:16:30 -08:00
|
|
|
isTriggerOrder={isTriggerOrder}
|
2023-07-16 20:41:13 -07:00
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
<SpotButtonGroup
|
|
|
|
minOrderDecimals={minOrderDecimals}
|
|
|
|
tickDecimals={tickDecimals}
|
|
|
|
useMargin={savedCheckboxSettings.margin}
|
2023-11-21 15:16:30 -08:00
|
|
|
isTriggerOrder={isTriggerOrder}
|
2023-07-16 20:41:13 -07:00
|
|
|
/>
|
|
|
|
)
|
|
|
|
) : tradeFormSizeUi === 'slider' ? (
|
|
|
|
<PerpSlider
|
|
|
|
minOrderDecimals={minOrderDecimals}
|
|
|
|
tickDecimals={tickDecimals}
|
|
|
|
/>
|
2022-11-22 15:49:42 -08:00
|
|
|
) : (
|
2023-07-16 20:41:13 -07:00
|
|
|
<PerpButtonGroup
|
|
|
|
minOrderDecimals={minOrderDecimals}
|
|
|
|
tickDecimals={tickDecimals}
|
|
|
|
/>
|
2022-11-22 15:49:42 -08:00
|
|
|
)}
|
2022-09-19 17:30:29 -07:00
|
|
|
</div>
|
2023-07-16 20:41:13 -07:00
|
|
|
<div className="flex flex-wrap px-5 md:flex-nowrap">
|
|
|
|
{tradeForm.tradeType === 'Limit' ? (
|
|
|
|
<div className="flex">
|
|
|
|
<div className="mr-3 mt-4" id="trade-step-six">
|
|
|
|
<Tooltip
|
|
|
|
className="hidden md:block"
|
|
|
|
delay={100}
|
|
|
|
placement="left"
|
|
|
|
content={t('trade:tooltip-post')}
|
|
|
|
>
|
|
|
|
<Checkbox
|
|
|
|
checked={tradeForm.postOnly}
|
|
|
|
onChange={(e) => handlePostOnlyChange(e.target.checked)}
|
|
|
|
>
|
|
|
|
{t('trade:post')}
|
|
|
|
</Checkbox>
|
|
|
|
</Tooltip>
|
|
|
|
</div>
|
|
|
|
<div className="mr-3 mt-4" id="trade-step-seven">
|
|
|
|
<Tooltip
|
|
|
|
className="hidden md:block"
|
|
|
|
delay={100}
|
|
|
|
placement="left"
|
|
|
|
content={t('trade:tooltip-ioc')}
|
|
|
|
>
|
|
|
|
<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}
|
2023-11-04 05:24:17 -07:00
|
|
|
{isTriggerOrder ? null : selectedMarket instanceof
|
|
|
|
Serum3Market ? (
|
2023-07-16 20:41:13 -07:00
|
|
|
<div className="mt-4" id="trade-step-eight">
|
|
|
|
<Tooltip
|
|
|
|
className="hidden md:block"
|
|
|
|
delay={100}
|
|
|
|
placement="left"
|
|
|
|
content={t('trade:tooltip-enable-margin')}
|
2023-03-23 20:06:16 -07:00
|
|
|
>
|
|
|
|
<Checkbox
|
2023-07-16 20:41:13 -07:00
|
|
|
checked={savedCheckboxSettings.margin}
|
2024-01-03 17:15:32 -08:00
|
|
|
disabled={isSanctioned}
|
2023-07-16 20:41:13 -07:00
|
|
|
onChange={handleSetMargin}
|
2023-03-23 20:06:16 -07:00
|
|
|
>
|
2023-07-16 20:41:13 -07:00
|
|
|
{t('trade:margin')}
|
2023-03-23 20:06:16 -07:00
|
|
|
</Checkbox>
|
2023-07-16 20:41:13 -07:00
|
|
|
</Tooltip>
|
|
|
|
</div>
|
|
|
|
) : (
|
|
|
|
<div className="mr-3 mt-4">
|
|
|
|
<Tooltip
|
|
|
|
className="hidden md:block"
|
|
|
|
delay={100}
|
|
|
|
placement="left"
|
|
|
|
content={
|
|
|
|
'Reduce will only decrease the size of an open position. This is often used for closing a position.'
|
|
|
|
}
|
2022-09-13 23:24:26 -07:00
|
|
|
>
|
2023-07-16 20:41:13 -07:00
|
|
|
<div className="flex items-center text-xs text-th-fgd-3">
|
|
|
|
<Checkbox
|
2023-10-25 06:32:54 -07:00
|
|
|
checked={
|
|
|
|
tradeForm.reduceOnly || isForceReduceOnly === true
|
|
|
|
}
|
2023-07-16 20:41:13 -07:00
|
|
|
onChange={(e) =>
|
|
|
|
handleReduceOnlyChange(e.target.checked)
|
|
|
|
}
|
2023-10-25 06:32:54 -07:00
|
|
|
disabled={isForceReduceOnly}
|
2023-07-16 20:41:13 -07:00
|
|
|
>
|
|
|
|
{t('trade:reduce-only')}
|
|
|
|
</Checkbox>
|
|
|
|
</div>
|
|
|
|
</Tooltip>
|
2023-01-16 20:00:42 -08:00
|
|
|
</div>
|
2023-07-16 20:41:13 -07:00
|
|
|
)}
|
2023-01-16 20:00:42 -08:00
|
|
|
</div>
|
2023-08-17 16:47:11 -07:00
|
|
|
<div className="mb-4 mt-6 flex px-3 md:px-4">
|
2023-12-02 03:43:22 -08:00
|
|
|
<TradeformSubmitButton
|
|
|
|
disabled={disabled}
|
2024-01-02 18:28:26 -08:00
|
|
|
isForceReduceOnly={isForceReduceOnly}
|
|
|
|
isSanctioned={isSanctioned}
|
2023-12-02 03:43:22 -08:00
|
|
|
placingOrder={placingOrder}
|
|
|
|
setShowCreateAccountModal={setShowCreateAccountModal}
|
|
|
|
setShowDepositModal={setShowDepositModal}
|
|
|
|
sideNames={sideNames}
|
2023-12-03 19:35:31 -08:00
|
|
|
tooMuchSize={tooMuchSize}
|
2023-12-02 03:43:22 -08:00
|
|
|
useMargin={savedCheckboxSettings.margin}
|
|
|
|
/>
|
2023-07-16 20:41:13 -07:00
|
|
|
</div>
|
|
|
|
</form>
|
2023-10-30 04:19:04 -07:00
|
|
|
{tradeForm.tradeType === 'Market' &&
|
|
|
|
selectedMarket instanceof PerpMarket ? (
|
2023-11-05 03:22:05 -08:00
|
|
|
<div className="mb-4 px-4">
|
2023-10-30 04:19:04 -07:00
|
|
|
<InlineNotification
|
|
|
|
type="warning"
|
|
|
|
desc={t('trade:price-expect')}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
) : null}
|
2023-12-07 02:39:46 -08:00
|
|
|
{perpSlotsFull && mangoAccountAddress ? (
|
|
|
|
<div className="mb-4 px-4">
|
|
|
|
<AccountSlotsFullNotification
|
|
|
|
message={t('trade:error-perp-positions-full')}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
) : null}
|
|
|
|
{serumSlotsFull && mangoAccountAddress ? (
|
2023-11-21 14:46:27 -08:00
|
|
|
<div className="mb-4 px-4">
|
2023-11-21 19:20:19 -08:00
|
|
|
<AccountSlotsFullNotification
|
|
|
|
message={t('trade:error-serum-positions-full')}
|
2023-11-21 14:46:27 -08:00
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
) : null}
|
2023-12-01 13:18:19 -08:00
|
|
|
{tokenPositionsFull &&
|
|
|
|
selectedMarket instanceof Serum3Market &&
|
|
|
|
mangoAccountAddress ? (
|
2023-11-21 16:30:19 -08:00
|
|
|
<div className="mb-4 px-4">
|
2023-11-21 19:20:19 -08:00
|
|
|
<AccountSlotsFullNotification
|
|
|
|
message={t('error-token-positions-full')}
|
2023-11-21 16:30:19 -08:00
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
) : null}
|
2023-08-28 04:59:36 -07:00
|
|
|
{borrowExceedsLimitInPeriod &&
|
|
|
|
remainingBorrowsInPeriod &&
|
|
|
|
timeToNextPeriod ? (
|
|
|
|
<div className="mb-4 px-4">
|
|
|
|
<InlineNotification
|
|
|
|
type="error"
|
|
|
|
desc={t('error-borrow-exceeds-limit', {
|
|
|
|
remaining: formatCurrencyValue(remainingBorrowsInPeriod),
|
|
|
|
resetTime: dayjs().to(
|
|
|
|
dayjs().add(timeToNextPeriod, 'second'),
|
|
|
|
),
|
|
|
|
})}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
) : null}
|
2024-01-03 17:15:32 -08:00
|
|
|
{isSanctioned && hasPosition ? (
|
|
|
|
<div className="mb-4 px-4">
|
|
|
|
<InlineNotification
|
|
|
|
type="error"
|
|
|
|
desc={t('trade:error-sanctioned-reduce-only')}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
) : null}
|
2023-11-05 03:22:05 -08:00
|
|
|
{isTriggerOrder ? (
|
|
|
|
<div className="mb-4 px-4">
|
|
|
|
<InlineNotification
|
|
|
|
desc={
|
|
|
|
<div>
|
|
|
|
<span className="mr-1">{t('swap:trigger-beta')}</span>
|
|
|
|
<Tooltip
|
|
|
|
content={
|
|
|
|
<ul className="ml-4 list-disc space-y-2">
|
|
|
|
<li>
|
|
|
|
Trigger orders on long-tail assets could be
|
|
|
|
susceptible to oracle manipulation.
|
|
|
|
</li>
|
|
|
|
<li>
|
|
|
|
Trigger orders rely on a sufficient amount of well
|
|
|
|
collateralized liquidators.
|
|
|
|
</li>
|
|
|
|
<li>
|
|
|
|
The slippage on existing orders could be
|
|
|
|
higher/lower than what's estimated.
|
|
|
|
</li>
|
|
|
|
<li>
|
|
|
|
The amount of tokens used to fill your order can
|
|
|
|
vary and depends on the final execution price.
|
|
|
|
</li>
|
|
|
|
</ul>
|
|
|
|
}
|
|
|
|
>
|
|
|
|
<span className="tooltip-underline whitespace-nowrap">
|
|
|
|
{t('swap:important-info')}
|
|
|
|
</span>
|
|
|
|
</Tooltip>
|
|
|
|
</div>
|
|
|
|
}
|
|
|
|
type="info"
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
) : null}
|
2023-08-28 04:59:36 -07:00
|
|
|
<TradeSummary balanceBank={balanceBank} mangoAccount={mangoAccount} />
|
2023-07-16 20:41:13 -07:00
|
|
|
</>
|
|
|
|
)}
|
2023-12-01 20:11:00 -08:00
|
|
|
{showDepositModal ? (
|
|
|
|
<DepositWithdrawModal
|
|
|
|
action="deposit"
|
|
|
|
isOpen={showDepositModal}
|
|
|
|
onClose={() => setShowDepositModal(false)}
|
|
|
|
token={
|
|
|
|
selectedMarket instanceof Serum3Market
|
|
|
|
? tradeForm.side === 'buy'
|
|
|
|
? quoteBank?.name
|
|
|
|
: baseBank?.name
|
|
|
|
: 'USDC'
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
) : null}
|
|
|
|
{showCreateAccountModal ? (
|
|
|
|
<CreateAccountModal
|
|
|
|
isOpen={showCreateAccountModal}
|
|
|
|
onClose={() => setShowCreateAccountModal(false)}
|
|
|
|
/>
|
|
|
|
) : null}
|
2022-09-13 23:24:26 -07:00
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
export default AdvancedTradeForm
|