import { useState, useCallback, useEffect, useMemo } from 'react' import { PublicKey } from '@solana/web3.js' import { ArrowDownIcon, ArrowRightIcon, Cog8ToothIcon, MagnifyingGlassIcon, ExclamationCircleIcon, HeartIcon, } from '@heroicons/react/20/solid' import { RouteInfo } from '@jup-ag/core' 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 { SwapLeverageSlider } from './LeverageSlider' import { useTranslation } from 'next-i18next' import SwapFormTokenList from './SwapFormTokenList' import { Transition } from '@headlessui/react' import Button, { IconButton, LinkButton } from '../shared/Button' import ButtonGroup from '../forms/ButtonGroup' import Loading from '../shared/Loading' import { EnterBottomExitBottom } from '../shared/Transitions' import useJupiter from './useJupiter' import SwapSettings from './SwapSettings' import SheenLoader from '../shared/SheenLoader' import { HealthType } from '@blockworks-foundation/mango-v4' import { INPUT_TOKEN_DEFAULT, OUTPUT_TOKEN_DEFAULT, } from '../../utils/constants' import { useTokenMax } from './useTokenMax' import WalletIcon from '../icons/WalletIcon' import Tooltip from '@components/shared/Tooltip' 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 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 = mangoStore.getState().group const set = mangoStore.getState().set const useMargin = mangoStore((s) => s.swap.margin) const slippage = mangoStore((s) => s.swap.slippage) const mangoAccount = mangoStore((s) => s.mangoAccount.current) const inputTokenInfo = mangoStore((s) => s.swap.inputTokenInfo) const outputTokenInfo = mangoStore((s) => s.swap.outputTokenInfo) const jupiterTokens = mangoStore((s) => s.jupiterTokens) const connected = mangoStore((s) => s.connected) const [debouncedAmountIn] = useDebounce(amountInFormValue, 300) const amountIn: Decimal | null = useMemo(() => { return Number(debouncedAmountIn) ? new Decimal(debouncedAmountIn) : new Decimal(0) }, [debouncedAmountIn]) const { amountOut, jupiter, routes } = useJupiter({ inputTokenInfo, outputTokenInfo, inputAmount: debouncedAmountIn, slippage, }) useEffect(() => { setSelectedRoute(routes[0]) }, [routes]) useEffect(() => { setAmountInFormValue('') }, [useMargin]) const handleAmountInChange = useCallback((e: NumberFormatValues) => { setAmountInFormValue(e.value) }, []) const handleTokenInSelect = useCallback( (mintAddress: string) => { const inputTokenInfo = jupiterTokens.find( (t: any) => t.address === mintAddress ) const group = mangoStore.getState().group if (group) { const bank = group.getFirstBankByMint(new PublicKey(mintAddress)) set((s) => { s.swap.inputBank = bank s.swap.inputTokenInfo = inputTokenInfo }) } setShowTokenSelect('') }, [jupiterTokens, set] ) const handleTokenOutSelect = useCallback( (mintAddress: string) => { const outputTokenInfo = jupiterTokens.find( (t: any) => t.address === mintAddress ) const group = mangoStore.getState().group if (group) { const bank = group.getFirstBankByMint(new PublicKey(mintAddress)) set((s) => { s.swap.outputBank = bank s.swap.outputTokenInfo = outputTokenInfo }) } setShowTokenSelect('') }, [jupiterTokens, set] ) const handleSwitchTokens = useCallback(() => { if (amountIn?.gt(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 s.swap.inputTokenInfo = outputTokenInfo s.swap.outputTokenInfo = inputTokenInfo }) setAnimateSwitchArrow( (prevanimateSwitchArrow) => prevanimateSwitchArrow + 1 ) }, [inputTokenInfo, outputTokenInfo, set, amountOut, amountIn]) const currentMaintHealth = useMemo(() => { if (!group || !mangoAccount) return 0 return mangoAccount.getHealthRatioUi(group, HealthType.maint) }, [mangoAccount]) const maintProjectedHealth = useMemo(() => { const group = mangoStore.getState().group if ( !inputTokenInfo || !mangoAccount || !outputTokenInfo || !amountOut || !group ) return 0 const simulatedHealthRatio = mangoAccount.simHealthRatioWithTokenPositionUiChanges( group, [ { mintPk: new PublicKey(inputTokenInfo.address), uiTokenAmount: amountIn.toNumber() * -1, }, { mintPk: new PublicKey(outputTokenInfo.address), uiTokenAmount: amountOut.toNumber(), }, ], HealthType.maint ) return simulatedHealthRatio! > 100 ? 100 : simulatedHealthRatio! < 0 ? 0 : Math.trunc(simulatedHealthRatio!) }, [mangoAccount, inputTokenInfo, outputTokenInfo, amountIn, amountOut]) const loadingSwapDetails: boolean = useMemo(() => { return ( !!amountIn.toNumber() && connected && (!selectedRoute || !outputTokenInfo) ) }, [amountIn, connected, selectedRoute, outputTokenInfo]) const showHealthImpact = !!inputTokenInfo && !!outputTokenInfo && !!amountOut return (
setShowConfirm(false)} amountIn={amountIn} slippage={slippage} jupiter={jupiter} routes={routes} selectedRoute={selectedRoute} setSelectedRoute={setSelectedRoute} /> setShowTokenSelect('')} onTokenSelect={ showTokenSelect === 'input' ? handleTokenInSelect : handleTokenOutSelect } type={showTokenSelect} useMargin={useMargin} /> setShowSettings(false)} />

{t('swap')}

setShowSettings(true)} size="small" >

{t('swap:from')}

{!useMargin ? ( ) : null}

{t('swap:to')}

{loadingSwapDetails ? (
) : ( {amountOut ? numberFormat.format(amountOut.toNumber()) : ''} )}
{useMargin ? ( <>

{t('leverage')}

{/*

0.00x

*/}
) : null}

{t('health-impact')}

{currentMaintHealth}%

15 ? 'text-th-orange' : maintProjectedHealth! <= 15 ? 'text-th-red' : 'text-th-green' } text-sm`} > {maintProjectedHealth!}%{' '} = currentMaintHealth! ? 'text-th-green' : 'text-th-red' }`} > ({maintProjectedHealth! >= currentMaintHealth! ? '+' : ''} {maintProjectedHealth! - currentMaintHealth!}%)

) } export default SwapForm const SwapFormSubmitButton = ({ amountIn, amountOut, inputSymbol, loadingSwapDetails, setShowConfirm, useMargin, }: { amountIn: Decimal amountOut: Decimal inputSymbol: string | undefined loadingSwapDetails: boolean setShowConfirm: (x: any) => any useMargin: boolean }) => { const { t } = useTranslation('common') const connected = mangoStore((s) => s.connected) const { amount: tokenMax, amountWithBorrow } = useTokenMax(useMargin) const showInsufficientBalance = useMargin ? amountWithBorrow.lt(amountIn) : tokenMax.lt(amountIn) const disabled = !amountIn.toNumber() || !connected || showInsufficientBalance || !amountOut.gt(0) 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, decimals } = useTokenMax(useMargin) useEffect(() => { if (tokenMax.gt(0) && amountIn && tokenMax.eq(amountIn)) { setSizePercentage('100') } }, [amountIn, tokenMax]) const handleSizePercentage = (percentage: string) => { setSizePercentage(percentage) if (tokenMax.gt(0)) { let amount = tokenMax.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="%" />
) }