import { useState, useCallback, useEffect, useMemo } from 'react' import { PublicKey } from '@solana/web3.js' import { ArrowDownIcon, Cog8ToothIcon, MagnifyingGlassIcon, ExclamationCircleIcon, LinkIcon, } from '@heroicons/react/20/solid' import NumberFormat, { NumberFormatValues } from 'react-number-format' import Decimal from 'decimal.js' import mangoStore from '@store/mangoStore' import ContentBox from '../shared/ContentBox' import JupiterRouteInfo from './JupiterRouteInfo' import TokenSelect from './TokenSelect' import useDebounce from '../shared/useDebounce' import { floorToDecimal, numberFormat } from '../../utils/numbers' import { useTranslation } from 'next-i18next' import SwapFormTokenList from './SwapFormTokenList' import { Transition } from '@headlessui/react' import Button, { IconButton } from '../shared/Button' import ButtonGroup from '../forms/ButtonGroup' import Loading from '../shared/Loading' import { EnterBottomExitBottom } from '../shared/Transitions' import useJupiterRoutes from './useJupiterRoutes' import SwapSettings from './SwapSettings' import SheenLoader from '../shared/SheenLoader' 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 MaxAmountButton from '@components/shared/MaxAmountButton' 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' const MAX_DIGITS = 11 export const withValueLimit = (values: NumberFormatValues): boolean => { return values.floatValue ? values.floatValue.toFixed(0).length <= MAX_DIGITS : true } const SwapForm = () => { const { t } = useTranslation(['common', 'swap']) const [selectedRoute, setSelectedRoute] = useState() const [amountInFormValue, setAmountInFormValue] = useState('') const [animateSwitchArrow, setAnimateSwitchArrow] = useState(0) const [showTokenSelect, setShowTokenSelect] = useState('') const [showSettings, setShowSettings] = useState(false) const [showConfirm, setShowConfirm] = useState(false) const { group } = useMangoGroup() const [swapFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'Slider') const set = mangoStore.getState().set const { margin: useMargin, slippage, inputBank, outputBank, } = mangoStore((s) => s.swap) const [debouncedAmountIn] = useDebounce(amountInFormValue, 300) const { mangoAccount } = useMangoAccount() const { connected } = useWallet() const amountIn: Decimal | null = useMemo(() => { return Number(debouncedAmountIn) ? new Decimal(debouncedAmountIn) : new Decimal(0) }, [debouncedAmountIn]) const { bestRoute, routes } = useJupiterRoutes({ inputMint: inputBank?.mint.toString() || USDC_MINT, outputMint: outputBank?.mint.toString() || MANGO_MINT, inputAmount: debouncedAmountIn, slippage, }) const outAmount: number = useMemo(() => { return selectedRoute?.outAmount.toString() ? new Decimal(selectedRoute.outAmount.toString()) .div(10 ** outputBank!.mintDecimals) .toNumber() : 0 }, [selectedRoute, outputBank]) useEffect(() => { if (bestRoute) { setSelectedRoute(bestRoute) } }, [bestRoute]) useEffect(() => { setAmountInFormValue('') }, [useMargin]) const handleAmountInChange = useCallback((e: NumberFormatValues) => { setAmountInFormValue(e.value) }, []) 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('') }, [set] ) 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('') }, [set] ) const handleSwitchTokens = useCallback(() => { if (amountIn?.gt(0) && outAmount) { setAmountInFormValue(outAmount.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 ) }, [set, outAmount, amountIn]) const maintProjectedHealth = useMemo(() => { const group = mangoStore.getState().group if (!inputBank || !mangoAccount || !outputBank || !outAmount || !group) return 0 const simulatedHealthRatio = mangoAccount.simHealthRatioWithTokenPositionUiChanges( group, [ { mintPk: inputBank.mint, uiTokenAmount: amountIn.toNumber() * -1, }, { mintPk: outputBank.mint, uiTokenAmount: outAmount, }, ], HealthType.maint ) return simulatedHealthRatio! > 100 ? 100 : simulatedHealthRatio! < 0 ? 0 : Math.trunc(simulatedHealthRatio!) }, [mangoAccount, inputBank, outputBank, amountIn, outAmount]) const loadingSwapDetails: boolean = useMemo(() => { return !!amountIn.toNumber() && connected && !selectedRoute }, [amountIn, connected, selectedRoute]) return (
setShowConfirm(false)} amountIn={amountIn} slippage={slippage} routes={routes} selectedRoute={selectedRoute} setSelectedRoute={setSelectedRoute} /> setShowTokenSelect('')} onTokenSelect={ showTokenSelect === 'input' ? handleTokenInSelect : handleTokenOutSelect } type={showTokenSelect} useMargin={useMargin} /> setShowSettings(false)} />
setShowSettings(true)} size="small" >

{t('swap:pay')}

{t('swap:receive')}

{loadingSwapDetails ? (
) : ( )}
{swapFormSizeUi === 'Slider' ? ( ) : ( )}
) } export default SwapForm const SwapFormSubmitButton = ({ amountIn, amountOut, inputSymbol, loadingSwapDetails, setShowConfirm, useMargin, }: { amountIn: Decimal amountOut: number | undefined inputSymbol: string | undefined loadingSwapDetails: boolean setShowConfirm: (x: any) => any useMargin: boolean }) => { const { t } = useTranslation('common') const { connected } = useWallet() const { amount: tokenMax, amountWithBorrow } = useTokenMax(useMargin) const showInsufficientBalance = useMargin ? amountWithBorrow.lt(amountIn) : tokenMax.lt(amountIn) const disabled = !amountIn.toNumber() || !connected || showInsufficientBalance || !amountOut return ( ) } const MaxSwapAmount = ({ setAmountIn, useMargin, }: { setAmountIn: (x: string) => void useMargin: boolean }) => { const { t } = useTranslation('common') const mangoAccountLoading = mangoStore((s) => s.mangoAccount.initialLoad) const { amount: tokenMax, amountWithBorrow, decimals, } = useTokenMax(useMargin) if (mangoAccountLoading) return null return (
setAmountIn(tokenMax.toFixed(decimals))} value={tokenMax.toFixed()} /> {useMargin ? ( setAmountIn(amountWithBorrow.toFixed(decimals))} value={amountWithBorrow.toFixed()} /> ) : null}
) } const PercentageSelectButtons = ({ amountIn, setAmountIn, useMargin, }: { amountIn: string setAmountIn: (x: string) => any useMargin: boolean }) => { const [sizePercentage, setSizePercentage] = useState('') const { amount: tokenMax, amountWithBorrow, decimals, } = useTokenMax(useMargin) const maxAmount = useMemo(() => { if (!tokenMax && !amountWithBorrow) return new Decimal(0) return useMargin ? amountWithBorrow : tokenMax }, [tokenMax, amountWithBorrow, useMargin]) useEffect(() => { if (maxAmount.gt(0) && amountIn && maxAmount.eq(amountIn)) { setSizePercentage('100') } }, [amountIn, maxAmount]) const handleSizePercentage = (percentage: string) => { setSizePercentage(percentage) if (maxAmount.gt(0)) { let amount = maxAmount.mul(percentage).div(100) if (percentage !== '100') { amount = floorToDecimal(amount, decimals) } setAmountIn(amount.toFixed()) } else { setAmountIn('0') } } return (
handleSizePercentage(p)} values={['10', '25', '50', '75', '100']} unit="%" />
) }