import { useState, useCallback, useEffect, useMemo, Dispatch, SetStateAction, } from 'react' import { ArrowDownIcon, ArrowDownTrayIcon } from '@heroicons/react/20/solid' import { NumberFormatValues, SourceInfo } from 'react-number-format' import Decimal from 'decimal.js' import mangoStore from '@store/mangoStore' import useDebounce from '../shared/useDebounce' import { MANGO_MINT, SIZE_INPUT_UI_KEY, USDC_MINT } from '../../utils/constants' import { useWallet } from '@solana/wallet-adapter-react' import { RouteInfo } from 'types/jupiter' import useLocalStorageState from 'hooks/useLocalStorageState' import SwapSlider from './SwapSlider' import PercentageSelectButtons from './PercentageSelectButtons' import BuyTokenInput from './BuyTokenInput' import SellTokenInput from './SellTokenInput' import Button from '@components/shared/Button' import { Transition } from '@headlessui/react' import SwapReviewRouteInfo from './SwapReviewRouteInfo' import useIpAddress from 'hooks/useIpAddress' import { useTranslation } from 'react-i18next' import useQuoteRoutes from './useQuoteRoutes' import { useTokenMax } from './useTokenMax' import Loading from '@components/shared/Loading' import InlineNotification from '@components/shared/InlineNotification' import useMangoAccount from 'hooks/useMangoAccount' import { toUiDecimalsForQuote } from '@blockworks-foundation/mango-v4' import DepositWithdrawModal from '@components/modals/DepositWithdrawModal' import useMangoAccountAccounts from 'hooks/useMangoAccountAccounts' import Link from 'next/link' import SecondaryConnectButton from '@components/shared/SecondaryConnectButton' import useRemainingBorrowsInPeriod from 'hooks/useRemainingBorrowsInPeriod' import dayjs from 'dayjs' import relativeTime from 'dayjs/plugin/relativeTime' import { formatCurrencyValue } from 'utils/numbers' dayjs.extend(relativeTime) type MarketSwapFormProps = { setShowTokenSelect: Dispatch> } const MAX_DIGITS = 11 export const withValueLimit = (values: NumberFormatValues): boolean => { return values.floatValue ? values.floatValue.toFixed(0).length <= MAX_DIGITS : true } export const NUMBER_FORMAT_CLASSNAMES = 'w-full rounded-r-lg h-[56px] box-border pb-4 border-l border-th-bkg-2 bg-th-input-bkg px-3 text-right font-mono text-xl text-th-fgd-1 focus:outline-none md:hover:bg-th-bkg-1' const set = mangoStore.getState().set const MarketSwapForm = ({ setShowTokenSelect }: MarketSwapFormProps) => { 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 [showConfirm, setShowConfirm] = useState(false) const [swapFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider') 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 { connected, publicKey } = useWallet() 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 { ipAllowed, ipCountry } = useIpAddress() 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 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, setSwapMode?: boolean) => { set((s) => { s.swap.amountOut = amountOut if (!parseFloat(amountOut)) { s.swap.amountIn = '' } if (setSwapMode) { s.swap.swapMode = 'ExactOut' } }) }, [], ) const handleAmountInChange = useCallback( (e: NumberFormatValues, info: SourceInfo) => { if (info.source !== 'event') return setAmountInFormValue(e.value) if (swapMode === 'ExactOut') { set((s) => { s.swap.swapMode = 'ExactIn' }) } }, [outputBank, setAmountInFormValue, swapMode], ) 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 handleMax = useCallback( (amountIn: string) => { setAmountInFormValue(amountIn, true) }, [setAmountInFormValue], ) const handleRepay = useCallback( (amountOut: string) => { setAmountOutFormValue(amountOut, true) }, [setAmountInFormValue], ) /* 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 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 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} />
{swapFormSizeUi === 'slider' ? ( setAmountInFormValue(v, true)} step={1 / 10 ** (inputBank?.mintDecimals || 6)} /> ) : (
setAmountInFormValue(v, true)} useMargin={useMargin} />
)}
{ipAllowed ? ( ) : ( )} ) } export default MarketSwapForm 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 { mangoAccountAddress } = useMangoAccount() const { connected } = useWallet() const { amount: tokenMax, amountWithBorrow } = useTokenMax(useMargin) const [showDepositModal, setShowDepositModal] = useState(false) const { usedTokens, totalTokens } = useMangoAccountAccounts() const { inputBank, outputBank } = mangoStore((s) => s.swap) const { remainingBorrowsInPeriod, timeToNextPeriod } = useRemainingBorrowsInPeriod(true) const tokenPositionsFull = useMemo(() => { if (!inputBank || !outputBank || !usedTokens.length || !totalTokens.length) return false const hasInputTokenPosition = usedTokens.find( (token) => token.tokenIndex === inputBank.tokenIndex, ) const hasOutputTokenPosition = usedTokens.find( (token) => token.tokenIndex === outputBank.tokenIndex, ) const availableTokenPositions = totalTokens.length - usedTokens.length if ( (hasInputTokenPosition && hasOutputTokenPosition) || availableTokenPositions >= 2 ) { return false } else if ( (hasInputTokenPosition && !hasOutputTokenPosition) || (!hasInputTokenPosition && hasOutputTokenPosition) ) { return availableTokenPositions >= 1 ? false : true } else return true }, [inputBank, outputBank, usedTokens, totalTokens]) const freeCollateral = useMemo(() => { const group = mangoStore.getState().group const mangoAccount = mangoStore.getState().mangoAccount.current return group && mangoAccount ? toUiDecimalsForQuote(mangoAccount.getCollateralValue(group)) : 0 }, [mangoAccountAddress]) const showInsufficientBalance = useMargin ? amountWithBorrow.lt(amountIn) || amountWithBorrow.eq(0) : tokenMax.lt(amountIn) || tokenMax.eq(0) // check if the borrowed amount exceeds the net borrow limit in the current period const borrowExceedsLimitInPeriod = useMemo(() => { const mangoAccount = mangoStore.getState().mangoAccount.current if (!mangoAccount || !inputBank) return false const balance = mangoAccount.getTokenDepositsUi(inputBank) const remainingBalance = balance - amountIn.toNumber() const borrowAmount = remainingBalance < 0 ? Math.abs(remainingBalance) : 0 return remainingBorrowsInPeriod ? borrowAmount > remainingBorrowsInPeriod : false }, [amountIn, inputBank, mangoAccountAddress, remainingBorrowsInPeriod]) const disabled = connected && !showInsufficientBalance && freeCollateral > 0 && (!amountIn.toNumber() || !amountOut || !selectedRoute || tokenPositionsFull || borrowExceedsLimitInPeriod) const onClick = showInsufficientBalance || freeCollateral <= 0 ? () => setShowDepositModal(true) : () => setShowConfirm(true) return ( <> {connected ? ( ) : ( )} {tokenPositionsFull ? (
{t('error-token-positions-full')}{' '} {t('manage')} } />
) : null} {borrowExceedsLimitInPeriod && remainingBorrowsInPeriod && timeToNextPeriod ? (
) : null} {selectedRoute === null && amountIn.gt(0) ? (
) : null} {showDepositModal ? ( setShowDepositModal(false)} token={freeCollateral > 0 ? inputSymbol : ''} /> ) : null} ) }