import { useState, useCallback, useEffect, useMemo } from 'react' import { PublicKey } from '@solana/web3.js' import { ArrowDownIcon, Cog8ToothIcon, ExclamationCircleIcon, LinkIcon, } from '@heroicons/react/20/solid' import NumberFormat, { NumberFormatValues, SourceInfo, } from 'react-number-format' import Decimal from 'decimal.js' import mangoStore from '@store/mangoStore' import ContentBox from '../shared/ContentBox' import SwapReviewRouteInfo from './SwapReviewRouteInfo' import TokenSelect from './TokenSelect' import { useTranslation } from 'next-i18next' import SwapFormTokenList from './SwapFormTokenList' import { Transition } from '@headlessui/react' import Button, { IconButton, LinkButton } from '../shared/Button' import Loading from '../shared/Loading' import { EnterBottomExitBottom } from '../shared/Transitions' import useQuoteRoutes from './useQuoteRoutes' import { HealthType } from '@blockworks-foundation/mango-v4' import { INPUT_TOKEN_DEFAULT, MANGO_MINT, OUTPUT_TOKEN_DEFAULT, SIZE_INPUT_UI_KEY, SWAP_MARGIN_KEY, USDC_MINT, } from '../../utils/constants' import { useTokenMax } from './useTokenMax' import HealthImpact from '@components/shared/HealthImpact' import { useWallet } from '@solana/wallet-adapter-react' import useMangoAccount from 'hooks/useMangoAccount' import { RouteInfo } from 'types/jupiter' import useMangoGroup from 'hooks/useMangoGroup' import useLocalStorageState from 'hooks/useLocalStorageState' import SwapSlider from './SwapSlider' import TokenVaultWarnings from '@components/shared/TokenVaultWarnings' import MaxSwapAmount from './MaxSwapAmount' import PercentageSelectButtons from './PercentageSelectButtons' import useIpAddress from 'hooks/useIpAddress' import SwapSettings from './SwapSettings' import InlineNotification from '@components/shared/InlineNotification' import useUnownedAccount from 'hooks/useUnownedAccount' import Tooltip from '@components/shared/Tooltip' import { formatCurrencyValue } from 'utils/numbers' import Switch from '@components/forms/Switch' import MaxAmountButton from '@components/shared/MaxAmountButton' const MAX_DIGITS = 11 export const withValueLimit = (values: NumberFormatValues): boolean => { return values.floatValue ? values.floatValue.toFixed(0).length <= MAX_DIGITS : true } const NUMBER_FORMAT_CLASSNAMES = 'w-full rounded-r-lg border h-[56px] border-th-input-border bg-th-input-bkg px-3 pb-4 border-box text-right font-mono text-xl 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' const set = mangoStore.getState().set const SwapForm = () => { const { t } = useTranslation(['common', 'swap', 'trade']) //initial state is undefined null is returned on error const [selectedRoute, setSelectedRoute] = useState() const [animateSwitchArrow, setAnimateSwitchArrow] = useState(0) const [showTokenSelect, setShowTokenSelect] = useState<'input' | 'output'>() const [showSettings, setShowSettings] = useState(false) const [showConfirm, setShowConfirm] = useState(false) const { group } = useMangoGroup() const [swapFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider') const [, setSavedSwapMargin] = useLocalStorageState( SWAP_MARGIN_KEY, true, ) const { ipAllowed, ipCountry } = useIpAddress() const { isUnownedAccount, isDelegatedAccount } = useUnownedAccount() const { margin: useMargin, slippage, inputBank, outputBank, amountIn: amountInFormValue, amountOut: amountOutFormValue, swapMode, } = mangoStore((s) => s.swap) const { mangoAccount } = useMangoAccount() const { connected, publicKey } = useWallet() const amountInAsDecimal: Decimal | null = useMemo(() => { return Number(amountInFormValue) ? new Decimal(amountInFormValue) : new Decimal(0) }, [amountInFormValue]) const amountOutAsDecimal: Decimal | null = useMemo(() => { return Number(amountOutFormValue) ? new Decimal(amountOutFormValue) : new Decimal(0) }, [amountOutFormValue]) const { bestRoute, routes } = useQuoteRoutes({ inputMint: inputBank?.mint.toString() || USDC_MINT, outputMint: outputBank?.mint.toString() || MANGO_MINT, amount: swapMode === 'ExactIn' ? amountInFormValue : amountOutFormValue, slippage, swapMode, wallet: publicKey?.toBase58(), mode: isDelegatedAccount ? 'JUPITER' : 'ALL', }) const setAmountInFormValue = useCallback( (amountIn: string, setSwapMode?: boolean) => { set((s) => { s.swap.amountIn = amountIn if (!parseFloat(amountIn)) { s.swap.amountOut = '' } if (setSwapMode) { s.swap.swapMode = 'ExactIn' } }) }, [], ) const setAmountFromSlider = useCallback( (amount: string) => { setAmountInFormValue(amount, true) }, [setAmountInFormValue], ) const setAmountOutFormValue = useCallback((amountOut: string) => { set((s) => { s.swap.amountOut = amountOut if (!parseFloat(amountOut)) { s.swap.amountIn = '' } }) }, []) const setBorrowAmountOut = useCallback( (borrowAmount: string) => { if (swapMode === 'ExactIn') { set((s) => { s.swap.swapMode = 'ExactOut' }) } setAmountOutFormValue(borrowAmount.toString()) }, [setAmountOutFormValue], ) /* Once a route is returned from the Jupiter API, use the inAmount or outAmount depending on the swapMode and set those values in state */ useEffect(() => { if (typeof bestRoute !== 'undefined') { setSelectedRoute(bestRoute) if (inputBank && swapMode === 'ExactOut' && bestRoute) { const inAmount = new Decimal(bestRoute!.inAmount) .div(10 ** inputBank.mintDecimals) .toString() setAmountInFormValue(inAmount) } else if (outputBank && swapMode === 'ExactIn' && bestRoute) { const outAmount = new Decimal(bestRoute!.outAmount) .div(10 ** outputBank.mintDecimals) .toString() setAmountOutFormValue(outAmount) } } }, [bestRoute, swapMode, inputBank, outputBank]) /* If the use margin setting is toggled, clear the form values */ useEffect(() => { setAmountInFormValue('') setAmountOutFormValue('') }, [useMargin, setAmountInFormValue, setAmountOutFormValue]) const handleAmountInChange = useCallback( (e: NumberFormatValues, info: SourceInfo) => { if (info.source !== 'event') return if (swapMode === 'ExactOut') { set((s) => { s.swap.swapMode = 'ExactIn' }) } setAmountInFormValue(e.value) }, [swapMode, setAmountInFormValue], ) const handleAmountOutChange = useCallback( (e: NumberFormatValues, info: SourceInfo) => { if (info.source !== 'event') return if (swapMode === 'ExactIn') { set((s) => { s.swap.swapMode = 'ExactOut' }) } setAmountOutFormValue(e.value) }, [swapMode, setAmountOutFormValue], ) const handleTokenInSelect = useCallback((mintAddress: string) => { const group = mangoStore.getState().group if (group) { const bank = group.getFirstBankByMint(new PublicKey(mintAddress)) set((s) => { s.swap.inputBank = bank }) } setShowTokenSelect(undefined) }, []) const handleTokenOutSelect = useCallback((mintAddress: string) => { const group = mangoStore.getState().group if (group) { const bank = group.getFirstBankByMint(new PublicKey(mintAddress)) set((s) => { s.swap.outputBank = bank }) } setShowTokenSelect(undefined) }, []) const handleSwitchTokens = useCallback( (amountIn: Decimal, amountOut: Decimal) => { if (amountIn?.gt(0) && amountOut.gte(0)) { setAmountInFormValue(amountOut.toString()) } const inputBank = mangoStore.getState().swap.inputBank const outputBank = mangoStore.getState().swap.outputBank set((s) => { s.swap.inputBank = outputBank s.swap.outputBank = inputBank }) setAnimateSwitchArrow( (prevanimateSwitchArrow) => prevanimateSwitchArrow + 1, ) }, [setAmountInFormValue], ) const maintProjectedHealth = useMemo(() => { const group = mangoStore.getState().group if ( !inputBank || !mangoAccount || !outputBank || !amountOutAsDecimal || !group ) return 0 const simulatedHealthRatio = mangoAccount.simHealthRatioWithTokenPositionUiChanges( group, [ { mintPk: inputBank.mint, uiTokenAmount: amountInAsDecimal.toNumber() * -1, }, { mintPk: outputBank.mint, uiTokenAmount: amountOutAsDecimal.toNumber(), }, ], HealthType.maint, ) return simulatedHealthRatio > 100 ? 100 : simulatedHealthRatio < 0 ? 0 : Math.trunc(simulatedHealthRatio) }, [ mangoAccount, inputBank, outputBank, amountInAsDecimal, amountOutAsDecimal, ]) const outputTokenBalanceBorrow = useMemo(() => { if (!outputBank) return 0 const balance = mangoAccount?.getTokenBalanceUi(outputBank) return balance && balance < 0 ? Math.abs(balance) : 0 }, [outputBank]) const loadingSwapDetails: boolean = useMemo(() => { return ( !!(amountInAsDecimal.toNumber() || amountOutAsDecimal.toNumber()) && connected && typeof selectedRoute === 'undefined' ) }, [amountInAsDecimal, amountOutAsDecimal, connected, selectedRoute]) const handleSetMargin = () => { set((s) => { s.swap.margin = !s.swap.margin }) } useEffect(() => { setSavedSwapMargin(useMargin) }, [useMargin]) return (
setShowConfirm(false)} amountIn={amountInAsDecimal} slippage={slippage} routes={routes} selectedRoute={selectedRoute} setSelectedRoute={setSelectedRoute} /> setShowTokenSelect(undefined)} onTokenSelect={ showTokenSelect === 'input' ? handleTokenInSelect : handleTokenOutSelect } type={showTokenSelect} useMargin={useMargin} /> setShowSettings(false)} />
setShowSettings(true)} >

{t('swap:pay')}

{!isUnownedAccount ? ( setAmountInFormValue(v, true)} /> ) : null}
{inputBank ? formatCurrencyValue( inputBank.uiPrice * Number(amountInFormValue), ) : '–'}

{t('swap:receive')}

{outputTokenBalanceBorrow ? ( setBorrowAmountOut( outputTokenBalanceBorrow.toFixed( outputBank?.mintDecimals || 9, ), ) } value={outputTokenBalanceBorrow} /> ) : null}
{loadingSwapDetails ? (
) : ( <> {outputBank ? formatCurrencyValue( outputBank.uiPrice * Number(amountOutFormValue), ) : '–'} )}
{swapFormSizeUi === 'slider' ? ( ) : ( )} {ipAllowed ? ( ) : ( )} {group && inputBank ? ( ) : null} {inputBank && inputBank.areBorrowsReduceOnly() && inputBank.areDepositsReduceOnly() ? (
) : null} {outputBank && outputBank.areBorrowsReduceOnly() && outputBank.areDepositsReduceOnly() ? (
) : null}

{t('swap:margin')}

{t('swap:max-slippage')}

setShowSettings(true)} > {slippage}%
) } export default SwapForm const SwapFormSubmitButton = ({ amountIn, amountOut, inputSymbol, loadingSwapDetails, selectedRoute, setShowConfirm, useMargin, }: { amountIn: Decimal amountOut: number | undefined inputSymbol: string | undefined loadingSwapDetails: boolean selectedRoute: RouteInfo | undefined | null setShowConfirm: (x: boolean) => void useMargin: boolean }) => { const { t } = useTranslation('common') const { connected, connect } = useWallet() const { amount: tokenMax, amountWithBorrow } = useTokenMax(useMargin) const showInsufficientBalance = useMargin ? amountWithBorrow.lt(amountIn) : tokenMax.lt(amountIn) const disabled = connected && (!amountIn.toNumber() || showInsufficientBalance || !amountOut || !selectedRoute) const onClick = connected ? () => setShowConfirm(true) : connect return ( <> {selectedRoute === null && amountIn.gt(0) ? (
) : null} ) }