import { HealthType, PerpMarket, PerpOrderSide, PerpOrderType, Serum3Market, Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side, } from '@blockworks-foundation/mango-v4' import Checkbox from '@components/forms/Checkbox' import Button from '@components/shared/Button' import TabButtons from '@components/shared/TabButtons' import Tooltip from '@components/shared/Tooltip' import mangoStore from '@store/mangoStore' import Decimal from 'decimal.js' import { useTranslation } from 'next-i18next' import { useCallback, useEffect, useMemo, useState } from 'react' import NumberFormat, { NumberFormatValues, SourceInfo, } from 'react-number-format' import { notify } from 'utils/notifications' import SpotSlider from './SpotSlider' import { calculateMarketPrice } from 'utils/tradeForm' import Image from 'next/legacy/image' import { QuestionMarkCircleIcon } from '@heroicons/react/20/solid' import Loading from '@components/shared/Loading' import TabUnderline from '@components/shared/TabUnderline' import PerpSlider from './PerpSlider' import HealthImpact from '@components/shared/HealthImpact' import useLocalStorageState from 'hooks/useLocalStorageState' import { SIZE_INPUT_UI_KEY } from 'utils/constants' import SpotButtonGroup from './SpotButtonGroup' import PerpButtonGroup from './PerpButtonGroup' import SolBalanceWarnings from '@components/shared/SolBalanceWarnings' import useJupiterMints from 'hooks/useJupiterMints' import useSelectedMarket from 'hooks/useSelectedMarket' import Slippage from './Slippage' import { formatFixedDecimals, getDecimalCount } from 'utils/numbers' import LogoWithFallback from '@components/shared/LogoWithFallback' const TABS: [string, number][] = [ ['Limit', 0], ['Market', 0], ] const set = mangoStore.getState().set const AdvancedTradeForm = () => { const { t } = useTranslation(['common', 'trade']) const tradeForm = mangoStore((s) => s.tradeForm) const { mangoTokens } = useJupiterMints() const { selectedMarket, price: oraclePrice } = useSelectedMarket() const [useMargin, setUseMargin] = useState(true) const [placingOrder, setPlacingOrder] = useState(false) const [tradeFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'Slider') const baseSymbol = useMemo(() => { return selectedMarket?.name.split(/-|\//)[0] }, [selectedMarket]) const baseLogoURI = useMemo(() => { if (!baseSymbol || !mangoTokens.length) return '' const token = mangoTokens.find((t) => t.symbol === baseSymbol) || mangoTokens.find((t) => t.symbol.includes(baseSymbol)) if (token) { return token.logoURI } return '' }, [baseSymbol, mangoTokens]) const quoteBank = useMemo(() => { const group = mangoStore.getState().group if (!group || !selectedMarket) return const tokenIdx = selectedMarket instanceof Serum3Market ? selectedMarket.quoteTokenIndex : selectedMarket?.settleTokenIndex return group?.getFirstBankByTokenIndex(tokenIdx) }, [selectedMarket]) const quoteSymbol = useMemo(() => { return quoteBank?.name }, [quoteBank]) const quoteLogoURI = useMemo(() => { if (!quoteSymbol || !mangoTokens.length) return '' const token = mangoTokens.find((t) => t.symbol === quoteSymbol) if (token) { return token.logoURI } return '' }, [quoteSymbol, mangoTokens]) const setTradeType = useCallback((tradeType: 'Limit' | 'Market') => { 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() } }) }, [] ) 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 = (price * parseFloat(e.value)).toString() } 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 = (parseFloat(e.value) / price).toString() } else { s.tradeForm.baseSize = '' } }) }, [oraclePrice] ) const handlePostOnlyChange = useCallback((postOnly: boolean) => { set((s) => { s.tradeForm.postOnly = postOnly if (s.tradeForm.ioc === true) { s.tradeForm.ioc = !postOnly } }) }, []) const handleIocChange = useCallback((ioc: boolean) => { set((s) => { s.tradeForm.ioc = ioc if (s.tradeForm.postOnly === true) { s.tradeForm.postOnly = !ioc } }) }, []) const handleSetSide = useCallback((side: 'buy' | 'sell') => { set((s) => { s.tradeForm.side = side }) }, []) /* * 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.toString() }) } }, [oraclePrice, 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 ) { let tickSize: number if (selectedMarket instanceof Serum3Market) { const market = group.getSerum3ExternalMarket( selectedMarket.serumMarketExternal ) tickSize = market.tickSize } else { tickSize = selectedMarket.tickSize } if (!isNaN(parseFloat(tradeForm.baseSize))) { const baseSize = new Decimal(tradeForm.baseSize)?.toNumber() const quoteSize = baseSize * oraclePrice set((s) => { s.tradeForm.price = oraclePrice.toFixed(getDecimalCount(tickSize)) s.tradeForm.quoteSize = quoteSize.toFixed(getDecimalCount(tickSize)) }) } else { set((s) => { s.tradeForm.price = oraclePrice.toFixed(getDecimalCount(tickSize)) }) } } }, [oraclePrice, selectedMarket, tradeForm]) const handlePlaceOrder = useCallback(async () => { const client = mangoStore.getState().client const group = mangoStore.getState().group const mangoAccount = mangoStore.getState().mangoAccount.current const tradeForm = mangoStore.getState().tradeForm const actions = mangoStore.getState().actions 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 = calculateMarketPrice(orderbook, baseSize, tradeForm.side) } if (selectedMarket instanceof Serum3Market) { const spotOrderType = tradeForm.ioc ? Serum3OrderType.immediateOrCancel : tradeForm.postOnly ? Serum3OrderType.postOnly : Serum3OrderType.limit const tx = await client.serum3PlaceOrder( group, mangoAccount, selectedMarket.serumMarketExternal, tradeForm.side === 'buy' ? Serum3Side.bid : Serum3Side.ask, price, baseSize, Serum3SelfTradeBehavior.decrementTake, spotOrderType, Date.now(), 10 ) actions.reloadMangoAccount() actions.fetchOpenOrders() notify({ type: 'success', title: 'Transaction successful', txid: tx, }) } else if (selectedMarket instanceof PerpMarket) { const perpOrderType = tradeForm.tradeType === 'Market' ? PerpOrderType.market : tradeForm.ioc ? PerpOrderType.immediateOrCancel : tradeForm.postOnly ? PerpOrderType.postOnly : PerpOrderType.limit const tx = await client.perpPlaceOrder( group, mangoAccount, selectedMarket.perpMarketIndex, tradeForm.side === 'buy' ? PerpOrderSide.bid : PerpOrderSide.ask, price, Math.abs(baseSize), undefined, // maxQuoteQuantity Date.now(), perpOrderType, undefined, undefined ) actions.reloadMangoAccount() actions.fetchOpenOrders() notify({ type: 'success', title: 'Transaction successful', txid: tx, }) } } catch (e: any) { notify({ title: 'There was an issue.', description: e.message, txid: e?.txid, type: 'error', }) console.error('Place trade error:', e) } finally { setPlacingOrder(false) } }, []) const maintProjectedHealth = useMemo(() => { const group = mangoStore.getState().group const mangoAccount = mangoStore.getState().mangoAccount.current if ( !mangoAccount || !group || !Number.isInteger(Number(tradeForm.baseSize)) ) return 100 let simulatedHealthRatio = 0 try { if (selectedMarket instanceof Serum3Market) { simulatedHealthRatio = tradeForm.side === 'sell' ? mangoAccount.simHealthRatioWithSerum3AskUiChanges( group, parseFloat(tradeForm.baseSize), selectedMarket.serumMarketExternal, HealthType.maint ) : mangoAccount.simHealthRatioWithSerum3BidUiChanges( group, parseFloat(tradeForm.baseSize), selectedMarket.serumMarketExternal, HealthType.maint ) } else if (selectedMarket instanceof PerpMarket) { simulatedHealthRatio = tradeForm.side === 'sell' ? mangoAccount.simHealthRatioWithPerpAskUiChanges( group, selectedMarket.perpMarketIndex, parseFloat(tradeForm.baseSize), Number(tradeForm.price) ) : mangoAccount.simHealthRatioWithPerpBidUiChanges( group, selectedMarket.perpMarketIndex, parseFloat(tradeForm.baseSize), Number(tradeForm.price) ) } } catch (e) { console.warn('Error calculating projected health: ', e) } return simulatedHealthRatio > 100 ? 100 : simulatedHealthRatio < 0 ? 0 : Math.trunc(simulatedHealthRatio) }, [selectedMarket, tradeForm]) return (
setTradeType(tab)} values={TABS} fillWidth />
handleSetSide(v)} />
{tradeForm.tradeType === 'Limit' ? ( <>

{t('trade:limit-price')}

{quoteLogoURI ? ( ) : ( )}
{quoteSymbol}
) : null}

{t('trade:amount')}

} />
{baseSymbol}
{quoteLogoURI ? ( ) : ( )}
{quoteSymbol}
{selectedMarket instanceof Serum3Market ? ( tradeFormSizeUi === 'Slider' ? ( ) : ( ) ) : tradeFormSizeUi === 'Slider' ? ( ) : ( )}
{tradeForm.tradeType === 'Limit' ? (
handlePostOnlyChange(e.target.checked)} > {t('trade:post')}
handleIocChange(e.target.checked)} > IOC
) : null} {selectedMarket instanceof Serum3Market ? (
setUseMargin(e.target.checked)} > {t('trade:margin')}
) : null}
{tradeForm.price && tradeForm.baseSize ? (

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

{formatFixedDecimals( parseFloat(tradeForm.price) * parseFloat(tradeForm.baseSize), true )}

) : null}
) } export default AdvancedTradeForm