import mangoStore from '@store/mangoStore' import NumberFormat, { NumberFormatValues, SourceInfo, } from 'react-number-format' import { DEFAULT_CHECKBOX_SETTINGS, INPUT_PREFIX_CLASSNAMES, INPUT_SUFFIX_CLASSNAMES, } from './AdvancedTradeForm' import LogoWithFallback from '@components/shared/LogoWithFallback' import { LinkIcon, QuestionMarkCircleIcon } from '@heroicons/react/20/solid' import useSelectedMarket from 'hooks/useSelectedMarket' import { useWallet } from '@solana/wallet-adapter-react' import useIpAddress from 'hooks/useIpAddress' import { useTranslation } from 'next-i18next' import { FormEvent, useCallback, useMemo, useState } from 'react' import Loading from '@components/shared/Loading' import Button from '@components/shared/Button' import Image from 'next/image' import useQuoteRoutes from '@components/swap/useQuoteRoutes' import { HealthType, Serum3Market } from '@blockworks-foundation/mango-v4' import Decimal from 'decimal.js' import { notify } from 'utils/notifications' import * as sentry from '@sentry/nextjs' import { isMangoError } from 'types' import SwapSlider from '@components/swap/SwapSlider' import PercentageSelectButtons from '@components/swap/PercentageSelectButtons' import { SIZE_INPUT_UI_KEY, TRADE_CHECKBOXES_KEY } from 'utils/constants' import useLocalStorageState from 'hooks/useLocalStorageState' import useUnownedAccount from 'hooks/useUnownedAccount' import HealthImpact from '@components/shared/HealthImpact' import Tooltip from '@components/shared/Tooltip' import Checkbox from '@components/forms/Checkbox' // import MaxMarketSwapAmount from './MaxMarketSwapAmount' import { floorToDecimal, formatNumericValue } from 'utils/numbers' import { formatTokenSymbol } from 'utils/tokens' import FormatNumericValue from '@components/shared/FormatNumericValue' import { useTokenMax } from '@components/swap/useTokenMax' import SheenLoader from '@components/shared/SheenLoader' import { fetchJupiterTransaction } from '@components/swap/SwapReviewRouteInfo' import MaxSwapAmount from '@components/swap/MaxSwapAmount' const set = mangoStore.getState().set function stringToNumberOrZero(s: string): number { const n = parseFloat(s) if (isNaN(n)) { return 0 } return n } export default function SpotMarketOrderSwapForm() { const { t } = useTranslation() const { baseSize, quoteSize, side } = mangoStore((s) => s.tradeForm) const { isUnownedAccount } = useUnownedAccount() const [placingOrder, setPlacingOrder] = useState(false) const { ipAllowed, ipCountry } = useIpAddress() const { connected, publicKey, connect } = useWallet() const [swapFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider') const [savedCheckboxSettings, setSavedCheckboxSettings] = useLocalStorageState(TRADE_CHECKBOXES_KEY, DEFAULT_CHECKBOX_SETTINGS) const { selectedMarket, price: oraclePrice, baseLogoURI, baseSymbol, quoteLogoURI, quoteSymbol, serumOrPerpMarket, } = useSelectedMarket() const { amount: tokenMax, amountWithBorrow } = useTokenMax( savedCheckboxSettings.margin, ) 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 setAmountFromSlider = useCallback( (amount: string) => { if (side === 'buy') { handleQuoteSizeChange( { value: amount } as NumberFormatValues, { source: 'event' } as SourceInfo, ) } else { handleBaseSizeChange( { value: amount } as NumberFormatValues, { source: 'event' } as SourceInfo, ) } }, [side, handleBaseSizeChange, handleQuoteSizeChange], ) const [inputBank, outputBank] = useMemo(() => { const group = mangoStore.getState().group if (!group || !(selectedMarket instanceof Serum3Market)) return [] const quoteBank = group?.getFirstBankByTokenIndex( selectedMarket.quoteTokenIndex, ) const baseBank = group.getFirstBankByTokenIndex( selectedMarket.baseTokenIndex, ) if (side === 'buy') { set((s) => { s.swap.inputBank = quoteBank s.swap.outputBank = baseBank }) return [quoteBank, baseBank] } else { set((s) => { s.swap.inputBank = baseBank s.swap.outputBank = quoteBank }) return [baseBank, quoteBank] } }, [selectedMarket, side]) const slippage = mangoStore.getState().swap.slippage const { bestRoute: selectedRoute, isLoading: loadingRoute } = useQuoteRoutes({ inputMint: inputBank?.mint.toString() || '', outputMint: outputBank?.mint.toString() || '', amount: side === 'buy' ? quoteSize : baseSize, slippage, swapMode: 'ExactIn', wallet: publicKey?.toBase58(), mode: 'JUPITER', }) const handlePlaceOrder = useCallback(async () => { const client = mangoStore.getState().client const group = mangoStore.getState().group const mangoAccount = mangoStore.getState().mangoAccount.current const { baseSize, quoteSize, side } = mangoStore.getState().tradeForm const actions = mangoStore.getState().actions const connection = mangoStore.getState().connection if ( !mangoAccount || !group || !inputBank || !outputBank || !publicKey || !selectedRoute ) return setPlacingOrder(true) const [ixs, alts] = await fetchJupiterTransaction( connection, selectedRoute, publicKey, slippage, inputBank.mint, outputBank.mint, ) try { const { signature: tx, slot } = await client.marginTrade({ group, mangoAccount, inputMintPk: inputBank.mint, amountIn: side === 'buy' ? stringToNumberOrZero(quoteSize) : stringToNumberOrZero(baseSize), outputMintPk: outputBank.mint, userDefinedInstructions: ixs, userDefinedAlts: alts, flashLoanType: { swap: {} }, }) set((s) => { s.successAnimation.swap = true }) // if (soundSettings['swap-success']) { // successSound.play() // } notify({ title: 'Transaction confirmed', type: 'success', txid: tx, noSound: true, }) actions.fetchGroup() actions.fetchSwapHistory(mangoAccount.publicKey.toString(), 30000) await actions.reloadMangoAccount(slot) set((s) => { s.tradeForm.baseSize = '' s.tradeForm.quoteSize = '' }) } catch (e) { console.error('onSwap error: ', e) sentry.captureException(e) if (isMangoError(e)) { notify({ title: 'Transaction failed', description: e.message, txid: e?.txid, type: 'error', }) } } finally { setPlacingOrder(false) } }, [inputBank, outputBank, publicKey, selectedRoute]) const handleSubmit = (e: FormEvent) => { e.preventDefault() connected ? handlePlaceOrder() : connect() } const maintProjectedHealth = useMemo(() => { const group = mangoStore.getState().group const mangoAccount = mangoStore.getState().mangoAccount.current if (!inputBank || !mangoAccount || !outputBank || !group) return 0 const simulatedHealthRatio = mangoAccount.simHealthRatioWithTokenPositionUiChanges( group, [ { mintPk: inputBank.mint, uiTokenAmount: (side === 'buy' ? stringToNumberOrZero(quoteSize) : stringToNumberOrZero(baseSize)) * -1, }, { mintPk: outputBank.mint, uiTokenAmount: side === 'buy' ? stringToNumberOrZero(baseSize) : stringToNumberOrZero(quoteSize), }, ], HealthType.maint, ) return simulatedHealthRatio > 100 ? 100 : simulatedHealthRatio < 0 ? 0 : Math.trunc(simulatedHealthRatio) }, [inputBank, outputBank, baseSize, quoteSize, side]) const [balance, borrowAmount] = useMemo(() => { if (!inputBank) return [0, 0] const mangoAccount = mangoStore.getState().mangoAccount.current if (!mangoAccount) return [0, 0] let borrowAmount const balance = mangoAccount.getTokenDepositsUi(inputBank) if (side === 'buy') { const remainingBalance = balance - parseFloat(quoteSize) borrowAmount = remainingBalance < 0 ? Math.abs(remainingBalance) : 0 } else { const remainingBalance = balance - parseFloat(baseSize) borrowAmount = remainingBalance < 0 ? Math.abs(remainingBalance) : 0 } return [balance, borrowAmount] }, [baseSize, inputBank, quoteSize]) const orderValue = useMemo(() => { if ( !inputBank || !outputBank || !oraclePrice || !baseSize || isNaN(parseFloat(baseSize)) ) return 0 const quotePriceDecimal = side === 'buy' ? new Decimal(inputBank.uiPrice) : new Decimal(outputBank.uiPrice) const basePriceDecimal = new Decimal(oraclePrice) const sizeDecimal = new Decimal(baseSize) return floorToDecimal( basePriceDecimal.mul(quotePriceDecimal).mul(sizeDecimal), 2, ) }, [baseSize, inputBank, outputBank, oraclePrice, side]) const tooMuchSize = useMemo(() => { if (!baseSize || !quoteSize || !amountWithBorrow || !tokenMax) return false const size = side === 'buy' ? new Decimal(quoteSize) : new Decimal(baseSize) const useMargin = savedCheckboxSettings.margin return useMargin ? size.gt(amountWithBorrow) : size.gt(tokenMax) }, [ amountWithBorrow, baseSize, quoteSize, side, tokenMax, savedCheckboxSettings.margin, ]) const disabled = (connected && (!baseSize || !oraclePrice)) || !serumOrPerpMarket || loadingRoute || tooMuchSize return ( <>
handleSubmit(e)}>
{!isUnownedAccount ? ( ) : null}
} />
{baseSymbol}
{quoteLogoURI ? (
) : (
)}
{quoteSymbol}
{swapFormSizeUi === 'slider' ? (
) : ( )}
setSavedCheckboxSettings({ ...savedCheckboxSettings, margin: e.target.checked, }) } > {t('trade:margin')}
{ipAllowed ? ( ) : ( )}

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

{orderValue ? ( ) : ( '–' )}

The price impact is the difference observed between the total value of the entry tokens swapped and the destination tokens obtained.

The bigger the trade is, the bigger the price impact can be.

} >

{t('swap:price-impact')}

{loadingRoute ? (
) : (

{selectedRoute ? selectedRoute?.priceImpactPct * 100 < 0.1 ? '<0.1%' : `${(selectedRoute?.priceImpactPct * 100).toFixed(2)}%` : '-'}

)}
{borrowAmount && inputBank ? ( <>

{t('borrow-amount')}

{' '} {formatTokenSymbol(inputBank.name)}

{t('loan-origination-fee')}

{' '} {formatTokenSymbol(inputBank.name)}

) : null}

{t('common:route')}

{loadingRoute ? (
) : (
{ let includeSeparator = false if ( selectedRoute?.marketInfos.length > 1 && index !== selectedRoute?.marketInfos.length - 1 ) { includeSeparator = true } return ( {`${info?.label} ${ includeSeparator ? 'x ' : '' }`} ) })} >
{selectedRoute?.marketInfos.map((info, index) => { let includeSeparator = false if ( selectedRoute?.marketInfos.length > 1 && index !== selectedRoute?.marketInfos.length - 1 ) { includeSeparator = true } return ( {`${info?.label} ${ includeSeparator ? 'x ' : '' }`} ) })}
)}
) }