diff --git a/components/swap/BuyTokenInput.tsx b/components/swap/BuyTokenInput.tsx index 30e00ee9..178df008 100644 --- a/components/swap/BuyTokenInput.tsx +++ b/components/swap/BuyTokenInput.tsx @@ -14,6 +14,7 @@ import { OUTPUT_TOKEN_DEFAULT } from 'utils/constants' import { NUMBER_FORMAT_CLASSNAMES } from './MarketSwapForm' import InlineNotification from '@components/shared/InlineNotification' import useMangoAccount from 'hooks/useMangoAccount' +import { SwapFormTokenListType } from './SwapFormTokenList' const BuyTokenInput = ({ error, @@ -25,7 +26,7 @@ const BuyTokenInput = ({ error?: string handleAmountOutChange: (e: NumberFormatValues, info: SourceInfo) => void loading?: boolean - setShowTokenSelect: Dispatch> + setShowTokenSelect: Dispatch> handleRepay?: (amountOut: string) => void }) => { const { t } = useTranslation('common') diff --git a/components/swap/LimitSwapForm.tsx b/components/swap/LimitSwapForm.tsx index ef94b595..8449b600 100644 --- a/components/swap/LimitSwapForm.tsx +++ b/components/swap/LimitSwapForm.tsx @@ -26,8 +26,8 @@ import SwapSlider from './SwapSlider' import PercentageSelectButtons from './PercentageSelectButtons' import { floorToDecimal, formatCurrencyValue } from 'utils/numbers' import { withValueLimit } from './MarketSwapForm' -import SellTokenInput from './SellTokenInput' -import BuyTokenInput from './BuyTokenInput' +import ReduceInputTokenInput from './ReduceInputTokenInput' +import ReduceOutputTokenInput from './ReduceOutputTokenInput' import { notify } from 'utils/notifications' import * as sentry from '@sentry/nextjs' import { isMangoError } from 'types' @@ -45,12 +45,13 @@ import DepositWithdrawModal from '@components/modals/DepositWithdrawModal' import useRemainingBorrowsInPeriod from 'hooks/useRemainingBorrowsInPeriod' import dayjs from 'dayjs' import relativeTime from 'dayjs/plugin/relativeTime' +import { SwapFormTokenListType } from './SwapFormTokenList' dayjs.extend(relativeTime) type LimitSwapFormProps = { - showTokenSelect: 'input' | 'output' | undefined - setShowTokenSelect: Dispatch> + showTokenSelect: SwapFormTokenListType + setShowTokenSelect: Dispatch> } type LimitSwapForm = { @@ -230,12 +231,20 @@ const LimitSwapForm = ({ return triggerDifference }, [quotePrice, triggerPrice]) - const handleTokenSelect = (type: 'input' | 'output') => { + const handleTokenSelect = (type: SwapFormTokenListType) => { setShowTokenSelect(type) setFormErrors({}) setTriggerPrice('') } + useLayoutEffect(() => { + if (!mangoAccount || !inputBank) { + return + } + const inputPos = mangoAccount.getTokenBalanceUi(inputBank) + setAnimateSwitchArrow(() => (inputPos > 0 ? 0 : 1)) + }, [inputBank, mangoAccount]) + const hasBorrowToRepay = useMemo(() => { if ( // orderType !== OrderTypes.REPAY_BORROW || @@ -480,47 +489,6 @@ const LimitSwapForm = ({ [amountInFormValue, flipPrices, setFormErrors, setTriggerPrice], ) - const handleSwitchTokens = useCallback(() => { - if (!inputBank || !outputBank) return - setFormErrors({}) - set((s) => { - s.swap.inputBank = outputBank - s.swap.outputBank = inputBank - }) - const multiplier = getOrderTypeMultiplier(orderType, flipPrices) - const price = flipPrices - ? floorToDecimal( - (inputBank.uiPrice / outputBank.uiPrice) * multiplier, - outputBank.mintDecimals, - ).toString() - : floorToDecimal( - (outputBank.uiPrice / inputBank.uiPrice) * multiplier, - inputBank.mintDecimals, - ).toString() - setTriggerPrice(price) - - if (amountInAsDecimal?.gt(0)) { - const amountOut = getAmountOut( - amountInAsDecimal.toString(), - flipPrices, - price, - ) - setAmountOutFormValue(amountOut.toString()) - } - setAnimateSwitchArrow( - (prevanimateSwitchArrow) => prevanimateSwitchArrow + 1, - ) - }, [ - amountInAsDecimal, - flipPrices, - inputBank, - orderType, - outputBank, - setAmountInFormValue, - setFormErrors, - triggerPrice, - ]) - const handlePlaceStopLoss = useCallback(async () => { const invalidFields = isFormValid({ amountIn: amountInAsDecimal.toNumber(), @@ -760,11 +728,11 @@ const LimitSwapForm = ({ return ( <> - handleTokenSelect('input')} + setShowTokenSelect={() => handleTokenSelect('reduce-input')} handleMax={handleMax} isTriggerOrder /> @@ -867,24 +835,19 @@ const LimitSwapForm = ({ ) : null}
- +
- handleTokenSelect('output')} + setShowTokenSelect={() => handleTokenSelect('reduce-output')} handleRepay={ // orderType === OrderTypes.REPAY_BORROW ? handleRepay diff --git a/components/swap/MarketSwapForm.tsx b/components/swap/MarketSwapForm.tsx index a39d3476..033ac93a 100644 --- a/components/swap/MarketSwapForm.tsx +++ b/components/swap/MarketSwapForm.tsx @@ -38,11 +38,12 @@ import useRemainingBorrowsInPeriod from 'hooks/useRemainingBorrowsInPeriod' import dayjs from 'dayjs' import relativeTime from 'dayjs/plugin/relativeTime' import { formatCurrencyValue } from 'utils/numbers' +import { SwapFormTokenListType } from './SwapFormTokenList' dayjs.extend(relativeTime) type MarketSwapFormProps = { - setShowTokenSelect: Dispatch> + setShowTokenSelect: Dispatch> } const MAX_DIGITS = 11 diff --git a/components/swap/MaxSwapAmount.tsx b/components/swap/MaxSwapAmount.tsx index ebf06675..484a4505 100644 --- a/components/swap/MaxSwapAmount.tsx +++ b/components/swap/MaxSwapAmount.tsx @@ -3,22 +3,20 @@ import mangoStore from '@store/mangoStore' import Decimal from 'decimal.js' import { useTranslation } from 'next-i18next' import { floorToDecimal } from 'utils/numbers' -import { useTokenMax } from './useTokenMax' +import { TokenMaxResults } from './useTokenMax' const MaxSwapAmount = ({ setAmountIn, useMargin, + maxAmount, }: { setAmountIn: (x: string) => void useMargin: boolean + maxAmount: (useMargin: boolean) => TokenMaxResults }) => { const { t } = useTranslation('common') const mangoAccountLoading = mangoStore((s) => s.mangoAccount.initialLoad) - const { - amount: tokenMax, - amountWithBorrow, - decimals, - } = useTokenMax(useMargin) + const { amount: tokenMax, amountWithBorrow, decimals } = maxAmount(useMargin) if (mangoAccountLoading) return null diff --git a/components/swap/ReduceInputTokenInput.tsx b/components/swap/ReduceInputTokenInput.tsx new file mode 100644 index 00000000..28cf2cb1 --- /dev/null +++ b/components/swap/ReduceInputTokenInput.tsx @@ -0,0 +1,149 @@ +import TokenSelect from './TokenSelect' +import NumberFormat, { + NumberFormatValues, + SourceInfo, +} from 'react-number-format' +import { formatCurrencyValue } from 'utils/numbers' +import { useTranslation } from 'react-i18next' +import { Dispatch, SetStateAction, useMemo } from 'react' +import mangoStore from '@store/mangoStore' +import useMangoGroup from 'hooks/useMangoGroup' +import { INPUT_TOKEN_DEFAULT } from 'utils/constants' +import { NUMBER_FORMAT_CLASSNAMES, withValueLimit } from './MarketSwapForm' +import MaxSwapAmount from './MaxSwapAmount' +import useUnownedAccount from 'hooks/useUnownedAccount' +import InlineNotification from '@components/shared/InlineNotification' +import useMangoAccount from 'hooks/useMangoAccount' +import { toUiDecimalsForQuote } from '@blockworks-foundation/mango-v4' +import { SwapFormTokenListType } from './SwapFormTokenList' +import { TokenMaxResults } from './useTokenMax' +import Decimal from 'decimal.js' + +const useAbsInputPosition = (): TokenMaxResults => { + const { mangoAccount } = useMangoAccount() + const { inputBank } = mangoStore((s) => s.swap) + + if (!mangoAccount || !inputBank) { + return { + amount: new Decimal(0), + amountWithBorrow: new Decimal(0), + decimals: 6, + } + } + + const amount = new Decimal( + Math.abs(mangoAccount.getTokenBalanceUi(inputBank)), + ) + return { + decimals: inputBank.mintDecimals, + amount: amount, + amountWithBorrow: amount, + } +} + +const ReduceInputTokenInput = ({ + handleAmountInChange, + setShowTokenSelect, + handleMax, + className, + error, + isTriggerOrder, +}: { + handleAmountInChange: (e: NumberFormatValues, info: SourceInfo) => void + setShowTokenSelect: Dispatch> + handleMax: (amountIn: string) => void + className?: string + error?: string + isTriggerOrder?: boolean +}) => { + const { t } = useTranslation('common') + const { mangoAccountAddress } = useMangoAccount() + const { group } = useMangoGroup() + const { isUnownedAccount } = useUnownedAccount() + const { + margin: useMargin, + inputBank, + amountIn: amountInFormValue, + } = mangoStore((s) => s.swap) + + const freeCollateral = useMemo(() => { + const group = mangoStore.getState().group + const mangoAccount = mangoStore.getState().mangoAccount.current + return group && mangoAccount + ? toUiDecimalsForQuote(mangoAccount.getCollateralValue(group)) + : 10 + }, [mangoAccountAddress]) + + return ( +
+
+

{t('reduce')}

+ {!isUnownedAccount ? ( + handleMax(v)} + maxAmount={useAbsInputPosition} + /> + ) : null} +
+
+ +
+
+ + {!isNaN(Number(amountInFormValue)) ? ( + + {inputBank + ? formatCurrencyValue( + inputBank.uiPrice * Number(amountInFormValue), + ) + : '–'} + + ) : null} +
+ {mangoAccountAddress && freeCollateral <= 0 ? ( +
+ +
+ ) : null} + {error ? ( +
+ +
+ ) : null} +
+ ) +} + +export default ReduceInputTokenInput diff --git a/components/swap/ReduceOutputTokenInput.tsx b/components/swap/ReduceOutputTokenInput.tsx new file mode 100644 index 00000000..15736ecb --- /dev/null +++ b/components/swap/ReduceOutputTokenInput.tsx @@ -0,0 +1,127 @@ +import MaxAmountButton from '@components/shared/MaxAmountButton' +import TokenSelect from './TokenSelect' +import Loading from '@components/shared/Loading' +import NumberFormat, { + NumberFormatValues, + SourceInfo, +} from 'react-number-format' +import { floorToDecimal, formatCurrencyValue } from 'utils/numbers' +import { useTranslation } from 'react-i18next' +import { Dispatch, SetStateAction, useMemo } from 'react' +import mangoStore from '@store/mangoStore' +import useMangoGroup from 'hooks/useMangoGroup' +import { OUTPUT_TOKEN_DEFAULT } from 'utils/constants' +import { NUMBER_FORMAT_CLASSNAMES } from './MarketSwapForm' +import InlineNotification from '@components/shared/InlineNotification' +import useMangoAccount from 'hooks/useMangoAccount' +import { SwapFormTokenListType } from './SwapFormTokenList' + +const ReduceOutputTokenInput = ({ + error, + handleAmountOutChange, + loading, + setShowTokenSelect, + handleRepay, +}: { + error?: string + handleAmountOutChange: (e: NumberFormatValues, info: SourceInfo) => void + loading?: boolean + setShowTokenSelect: Dispatch> + handleRepay?: (amountOut: string) => void +}) => { + const { t } = useTranslation('common') + const { mangoAccount } = useMangoAccount() + const { group } = useMangoGroup() + const { + inputBank, + outputBank, + amountOut: amountOutFormValue, + } = mangoStore((s) => s.swap) + + const reducingLong = + mangoAccount && inputBank + ? mangoAccount.getTokenBalanceUi(inputBank) > 0 + : false + + const outputTokenBalanceBorrow = useMemo(() => { + if (!outputBank || !mangoAccount) return 0 + const balance = mangoAccount.getTokenBalanceUi(outputBank) + const roundedBalance = floorToDecimal( + balance, + outputBank.mintDecimals, + ).toNumber() + return balance && balance < 0 ? Math.abs(roundedBalance).toString() : 0 + }, [mangoAccount, outputBank]) + + return ( +
+
+

+ {reducingLong ? t('producing') : t('by-selling')} +

+ {handleRepay && outputTokenBalanceBorrow ? ( + handleRepay(outputTokenBalanceBorrow)} + value={outputTokenBalanceBorrow} + /> + ) : null} +
+
+ +
+
+ {loading ? ( +
+ +
+ ) : ( + <> + + {!isNaN(Number(amountOutFormValue)) ? ( + + {outputBank + ? formatCurrencyValue( + outputBank.uiPrice * Number(amountOutFormValue), + ) + : '–'} + + ) : null} + + )} +
+ {error ? ( +
+ +
+ ) : null} +
+ ) +} + +export default ReduceOutputTokenInput diff --git a/components/swap/SellTokenInput.tsx b/components/swap/SellTokenInput.tsx index 05f89189..abd4517a 100644 --- a/components/swap/SellTokenInput.tsx +++ b/components/swap/SellTokenInput.tsx @@ -15,6 +15,8 @@ import useUnownedAccount from 'hooks/useUnownedAccount' import InlineNotification from '@components/shared/InlineNotification' import useMangoAccount from 'hooks/useMangoAccount' import { toUiDecimalsForQuote } from '@blockworks-foundation/mango-v4' +import { SwapFormTokenListType } from './SwapFormTokenList' +import { useTokenMax } from './useTokenMax' const SellTokenInput = ({ handleAmountInChange, @@ -25,7 +27,7 @@ const SellTokenInput = ({ isTriggerOrder, }: { handleAmountInChange: (e: NumberFormatValues, info: SourceInfo) => void - setShowTokenSelect: Dispatch> + setShowTokenSelect: Dispatch> handleMax: (amountIn: string) => void className?: string error?: string @@ -59,6 +61,7 @@ const SellTokenInput = ({ handleMax(v)} + maxAmount={useTokenMax} /> ) : null} diff --git a/components/swap/SwapForm.tsx b/components/swap/SwapForm.tsx index 7fae0043..269f9727 100644 --- a/components/swap/SwapForm.tsx +++ b/components/swap/SwapForm.tsx @@ -20,13 +20,15 @@ import LimitSwapForm from './LimitSwapForm' import Switch from '@components/forms/Switch' import useLocalStorageState from 'hooks/useLocalStorageState' import { useIsWhiteListed } from 'hooks/useIsWhiteListed' +import { SwapFormTokenListType } from './SwapFormTokenList' const set = mangoStore.getState().set const SwapForm = () => { const { t } = useTranslation(['common', 'swap', 'trade']) const { data: isWhiteListed } = useIsWhiteListed() - const [showTokenSelect, setShowTokenSelect] = useState<'input' | 'output'>() + const [showTokenSelect, setShowTokenSelect] = + useState() const [showSettings, setShowSettings] = useState(false) const [swapOrLimit, setSwapOrLimit] = useState('swap') const [, setSavedSwapMargin] = useLocalStorageState( @@ -149,7 +151,7 @@ const SwapForm = () => { setShowTokenSelect(undefined)} onTokenSelect={ - showTokenSelect === 'input' + showTokenSelect === 'input' || showTokenSelect === 'reduce-input' ? handleTokenInSelect : handleTokenOutSelect } diff --git a/components/swap/SwapFormTokenList.tsx b/components/swap/SwapFormTokenList.tsx index e00a2d3e..d87e8f57 100644 --- a/components/swap/SwapFormTokenList.tsx +++ b/components/swap/SwapFormTokenList.tsx @@ -15,6 +15,13 @@ import { formatTokenSymbol } from 'utils/tokens' import TokenLogo from '@components/shared/TokenLogo' import Input from '@components/forms/Input' +export type SwapFormTokenListType = + | 'input' + | 'output' + | 'reduce-input' + | 'reduce-output' + | undefined + const generateSearchTerm = (item: Token, searchValue: string) => { const normalizedSearchValue = searchValue.toLowerCase() const values = `${item.symbol} ${item.name}`.toLowerCase() @@ -50,7 +57,7 @@ const TokenItem = ({ token: TokenInfoWithAmounts onSubmit: (x: string) => void useMargin: boolean - type: 'input' | 'output' | undefined + type: SwapFormTokenListType }) => { const { t } = useTranslation('trade') const { address, symbol, name } = token @@ -92,7 +99,7 @@ const TokenItem = ({

- {type === 'input' && + {(type === 'input' || type === 'reduce-input') && token.amount && token.amountWithBorrow && token.decimals ? ( @@ -128,7 +135,7 @@ const SwapFormTokenList = ({ }: { onClose: () => void onTokenSelect: (x: string) => void - type: 'input' | 'output' | undefined + type: SwapFormTokenListType useMargin: boolean }) => { const { t } = useTranslation(['common', 'search', 'swap']) @@ -177,6 +184,37 @@ const SwapFormTokenList = ({ : Number(b.amount) - Number(a.amount), ) + return filteredSortedTokens + } else if ( + mangoTokens?.length && + group && + mangoAccount && + outputBank && + inputBank && + type === 'reduce-input' + ) { + const filteredSortedTokens = mangoTokens + .map((token) => { + const tokenBank = group.getFirstBankByMint( + new PublicKey(token.address), + ) + const uiAmount = mangoAccount.getTokenBalanceUi(tokenBank) + const uiDollarValue = uiAmount * tokenBank.uiPrice + console.log(tokenBank) + return { + ...token, + amount: new Decimal(uiAmount), + amountWithBorrow: new Decimal(uiAmount), + absDollarValue: Math.abs(uiDollarValue), + decimals: inputBank.mintDecimals, + } + }) + .filter( + (token) => + token.symbol !== outputBank?.name && token.absDollarValue > 0.0001, + ) + .sort((a, b) => b.absDollarValue - a.absDollarValue) + return filteredSortedTokens } else if (mangoTokens?.length) { const filteredTokens = mangoTokens @@ -212,6 +250,8 @@ const SwapFormTokenList = ({ ? t('swap:you-sell') : type === 'output' ? t('swap:you-buy') + : type === 'reduce-input' + ? t('swap:you-reduce') : ''}

> - type: 'input' | 'output' + showTokenList: Dispatch> + type: SwapFormTokenListType } const TokenSelect = ({ bank, showTokenList, type }: TokenSelectProps) => { const { group } = useMangoGroup() + const { mangoAccount } = useMangoAccount() if (!group) return null + let posType = '' + if (type === 'reduce-input' && mangoAccount && bank) { + const uiPos = mangoAccount.getTokenBalanceUi(bank) + if (uiPos > 0) { + posType = 'long' + } else if (uiPos < 0) { + posType = 'short' + } + } + return (