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 useDebounce from '../shared/useDebounce' 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, 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 { useEnhancedWallet } from '@components/wallet/EnhancedWalletProvider' import SwapSettings from './SwapSettings' import InlineNotification from '@components/shared/InlineNotification' import useUnownedAccount from 'hooks/useUnownedAccount' import Tooltip from '@components/shared/Tooltip' 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-lg rounded-l-none border border-th-input-border bg-th-input-bkg p-3 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 { ipAllowed, ipCountry } = useIpAddress() const { isUnownedAccount } = useUnownedAccount() const { margin: useMargin, slippage, inputBank, outputBank, amountIn: amountInFormValue, amountOut: amountOutFormValue, swapMode, } = mangoStore((s) => s.swap) const [debouncedAmountIn] = useDebounce(amountInFormValue, 300) const [debouncedAmountOut] = useDebounce(amountOutFormValue, 300) const { mangoAccount } = useMangoAccount() const { isDelegatedAccount } = useUnownedAccount() const { connected, publicKey } = useWallet() const amountInAsDecimal: Decimal | null = useMemo(() => { return Number(debouncedAmountIn) ? new Decimal(debouncedAmountIn) : new Decimal(0) }, [debouncedAmountIn]) const amountOutAsDecimal: Decimal | null = useMemo(() => { return Number(debouncedAmountOut) ? new Decimal(debouncedAmountOut) : new Decimal(0) }, [debouncedAmountOut]) const { bestRoute, routes } = useQuoteRoutes({ inputMint: inputBank?.mint.toString() || USDC_MINT, outputMint: outputBank?.mint.toString() || MANGO_MINT, amount: swapMode === 'ExactIn' ? debouncedAmountIn : debouncedAmountOut, slippage, swapMode, wallet: publicKey?.toBase58(), }) 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 setAmountOutFormValue = useCallback((amountOut: string) => { set((s) => { s.swap.amountOut = amountOut if (!parseFloat(amountOut)) { s.swap.amountIn = '' } }) }, []) /* 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(() => { if (amountInAsDecimal?.gt(0) && amountOutAsDecimal.gte(0)) { setAmountInFormValue(amountOutAsDecimal.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, amountOutAsDecimal, amountInAsDecimal]) 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 loadingSwapDetails: boolean = useMemo(() => { return ( !!(amountInAsDecimal.toNumber() || amountOutAsDecimal.toNumber()) && connected && typeof selectedRoute === 'undefined' ) }, [amountInAsDecimal, amountOutAsDecimal, connected, selectedRoute]) 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}

{t('swap:receive')}

{loadingSwapDetails ? (
) : ( )}
{swapFormSizeUi === 'slider' ? ( setAmountInFormValue(v, true)} step={1 / 10 ** (inputBank?.mintDecimals || 6)} /> ) : ( setAmountInFormValue(v, true)} useMargin={useMargin} /> )} {ipAllowed ? ( ) : ( )} {group && inputBank ? ( ) : null} {inputBank && inputBank.areBorrowsReduceOnly() && inputBank.areDepositsReduceOnly() ? (
) : null} {outputBank && outputBank.areBorrowsReduceOnly() && outputBank.areDepositsReduceOnly() ? (
) : null}

{t('swap:margin')}

setShowSettings(true)} > {useMargin ? t('swap:enabled') : t('swap:disabled')}

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

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