This commit is contained in:
Christian Kamm 2023-09-06 15:06:57 +02:00
parent 44b827da32
commit 1bf1514cc0
12 changed files with 379 additions and 81 deletions

View File

@ -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<SetStateAction<'input' | 'output' | undefined>>
setShowTokenSelect: Dispatch<SetStateAction<SwapFormTokenListType>>
handleRepay?: (amountOut: string) => void
}) => {
const { t } = useTranslation('common')

View File

@ -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<SetStateAction<'input' | 'output' | undefined>>
showTokenSelect: SwapFormTokenListType
setShowTokenSelect: Dispatch<SetStateAction<SwapFormTokenListType>>
}
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 (
<>
<SellTokenInput
<ReduceInputTokenInput
className="rounded-b-none"
error={formErrors.amountIn}
handleAmountInChange={handleAmountInChange}
setShowTokenSelect={() => handleTokenSelect('input')}
setShowTokenSelect={() => handleTokenSelect('reduce-input')}
handleMax={handleMax}
isTriggerOrder
/>
@ -867,24 +835,19 @@ const LimitSwapForm = ({
) : null}
</div>
<div className="my-2 flex justify-center">
<button
className="rounded-full border border-th-fgd-4 p-1.5 text-th-fgd-3 focus-visible:border-th-active md:hover:border-th-active md:hover:text-th-active"
onClick={handleSwitchTokens}
>
<ArrowDownIcon
className="h-5 w-5"
style={
animateSwitchArrow % 2 == 0
? { transform: 'rotate(0deg)' }
: { transform: 'rotate(360deg)' }
: { transform: 'rotate(180deg)' }
}
/>
</button>
</div>
<BuyTokenInput
<ReduceOutputTokenInput
error={formErrors.hasBorrows}
handleAmountOutChange={handleAmountOutChange}
setShowTokenSelect={() => handleTokenSelect('output')}
setShowTokenSelect={() => handleTokenSelect('reduce-output')}
handleRepay={
// orderType === OrderTypes.REPAY_BORROW ?
handleRepay

View File

@ -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<SetStateAction<'input' | 'output' | undefined>>
setShowTokenSelect: Dispatch<SetStateAction<SwapFormTokenListType>>
}
const MAX_DIGITS = 11

View File

@ -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

View File

@ -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<SetStateAction<SwapFormTokenListType>>
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 (
<div
className={`grid grid-cols-2 rounded-t-xl bg-th-bkg-2 p-3 pb-2 ${className}`}
>
<div className="col-span-2 mb-2 flex items-center justify-between">
<p className="text-th-fgd-2">{t('reduce')}</p>
{!isUnownedAccount ? (
<MaxSwapAmount
useMargin={isTriggerOrder ? false : useMargin}
setAmountIn={(v) => handleMax(v)}
maxAmount={useAbsInputPosition}
/>
) : null}
</div>
<div className="col-span-1">
<TokenSelect
bank={
inputBank || group?.banksMapByName.get(INPUT_TOKEN_DEFAULT)?.[0] // default to a user position
}
showTokenList={setShowTokenSelect}
type="reduce-input"
/>
</div>
<div className="relative col-span-1">
<NumberFormat
inputMode="decimal"
thousandSeparator=","
allowNegative={false}
isNumericString={true}
decimalScale={inputBank?.mintDecimals || 6}
name="amountIn"
id="amountIn"
className={NUMBER_FORMAT_CLASSNAMES}
placeholder="0.00"
value={amountInFormValue}
onValueChange={handleAmountInChange}
isAllowed={withValueLimit}
/>
{!isNaN(Number(amountInFormValue)) ? (
<span className="absolute bottom-1.5 right-3 text-xxs text-th-fgd-4">
{inputBank
? formatCurrencyValue(
inputBank.uiPrice * Number(amountInFormValue),
)
: ''}
</span>
) : null}
</div>
{mangoAccountAddress && freeCollateral <= 0 ? (
<div className="col-span-2 mt-1 flex justify-center">
<InlineNotification
type="warning"
desc={t('swap:warning-no-collateral')}
hideBorder
hidePadding
/>
</div>
) : null}
{error ? (
<div className="col-span-2 mt-1 flex justify-center">
<InlineNotification
type="error"
desc={error}
hideBorder
hidePadding
/>
</div>
) : null}
</div>
)
}
export default ReduceInputTokenInput

View File

@ -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<SetStateAction<SwapFormTokenListType>>
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 (
<div className="mb-2 grid grid-cols-2 rounded-xl bg-th-bkg-2 p-3">
<div className="col-span-2 mb-2 flex items-end justify-between">
<p className="text-th-fgd-2">
{reducingLong ? t('producing') : t('by-selling')}
</p>
{handleRepay && outputTokenBalanceBorrow ? (
<MaxAmountButton
className="mb-0.5 text-xs"
decimals={outputBank?.mintDecimals || 9}
label={t('repay')}
onClick={() => handleRepay(outputTokenBalanceBorrow)}
value={outputTokenBalanceBorrow}
/>
) : null}
</div>
<div className="col-span-1">
<TokenSelect
bank={
outputBank || group?.banksMapByName.get(OUTPUT_TOKEN_DEFAULT)?.[0]
}
showTokenList={setShowTokenSelect}
type="reduce-output"
/>
</div>
<div className="relative col-span-1">
{loading ? (
<div className="flex h-[56px] w-full items-center justify-center rounded-l-none rounded-r-lg border-l border-th-bkg-2 bg-th-input-bkg">
<Loading />
</div>
) : (
<>
<NumberFormat
inputMode="decimal"
thousandSeparator=","
allowNegative={false}
isNumericString={true}
decimalScale={outputBank?.mintDecimals || 6}
name="amountOut"
id="amountOut"
className={NUMBER_FORMAT_CLASSNAMES}
placeholder="0.00"
value={amountOutFormValue}
onValueChange={handleAmountOutChange}
/>
{!isNaN(Number(amountOutFormValue)) ? (
<span className="absolute bottom-1.5 right-3 text-xxs text-th-fgd-4">
{outputBank
? formatCurrencyValue(
outputBank.uiPrice * Number(amountOutFormValue),
)
: ''}
</span>
) : null}
</>
)}
</div>
{error ? (
<div className="col-span-2 mt-1 flex justify-center">
<InlineNotification
type="error"
desc={error}
hideBorder
hidePadding
/>
</div>
) : null}
</div>
)
}
export default ReduceOutputTokenInput

View File

@ -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<SetStateAction<'input' | 'output' | undefined>>
setShowTokenSelect: Dispatch<SetStateAction<SwapFormTokenListType>>
handleMax: (amountIn: string) => void
className?: string
error?: string
@ -59,6 +61,7 @@ const SellTokenInput = ({
<MaxSwapAmount
useMargin={isTriggerOrder ? false : useMargin}
setAmountIn={(v) => handleMax(v)}
maxAmount={useTokenMax}
/>
) : null}
</div>

View File

@ -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<SwapFormTokenListType>()
const [showSettings, setShowSettings] = useState(false)
const [swapOrLimit, setSwapOrLimit] = useState('swap')
const [, setSavedSwapMargin] = useLocalStorageState<boolean>(
@ -149,7 +151,7 @@ const SwapForm = () => {
<SwapFormTokenList
onClose={() => setShowTokenSelect(undefined)}
onTokenSelect={
showTokenSelect === 'input'
showTokenSelect === 'input' || showTokenSelect === 'reduce-input'
? handleTokenInSelect
: handleTokenOutSelect
}

View File

@ -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 = ({
</p>
</div>
</div>
{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')
: ''}
</p>
<IconButton

View File

@ -4,18 +4,31 @@ import { Bank } from '@blockworks-foundation/mango-v4'
import { Dispatch, SetStateAction } from 'react'
import { formatTokenSymbol } from 'utils/tokens'
import TokenLogo from '@components/shared/TokenLogo'
import { SwapFormTokenListType } from './SwapFormTokenList'
import useMangoAccount from 'hooks/useMangoAccount'
type TokenSelectProps = {
bank: Bank | undefined
showTokenList: Dispatch<SetStateAction<'input' | 'output' | undefined>>
type: 'input' | 'output'
showTokenList: Dispatch<SetStateAction<SwapFormTokenListType>>
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 (
<button
onClick={() => showTokenList(type)}
@ -26,7 +39,7 @@ const TokenSelect = ({ bank, showTokenList, type }: TokenSelectProps) => {
</div>
<div className="flex w-full items-center justify-between">
<div className="text-xl font-bold text-th-fgd-1">
{formatTokenSymbol(bank!.name)}
{formatTokenSymbol(bank!.name)} {posType}
</div>
<ChevronDownIcon className="h-6 w-6" />
</div>

View File

@ -105,7 +105,7 @@ export const getTokenInMax = (
}
}
interface TokenMaxResults {
export interface TokenMaxResults {
amount: Decimal
amountWithBorrow: Decimal
decimals: number

View File

@ -352,6 +352,7 @@ export default function SpotMarketOrderSwapForm() {
<MaxSwapAmount
useMargin={savedCheckboxSettings.margin}
setAmountIn={setAmountFromSlider}
maxAmount={useTokenMax}
/>
) : null}
<div className="flex flex-col">