add missing parts to market swap trade form

This commit is contained in:
saml33 2023-07-25 12:35:27 +10:00
parent 01f03022bf
commit a151aa5190
4 changed files with 318 additions and 114 deletions

View File

@ -4,14 +4,18 @@ import { useEffect, useMemo, useState } from 'react'
import { floorToDecimal } from 'utils/numbers'
import { useTokenMax } from './useTokenMax'
const DEFAULT_VALUES = ['10', '25', '50', '75', '100']
const PercentageSelectButtons = ({
amountIn,
setAmountIn,
useMargin,
values,
}: {
amountIn: string
setAmountIn: (x: string) => void
useMargin: boolean
values?: string[]
}) => {
const [sizePercentage, setSizePercentage] = useState('')
const {
@ -49,7 +53,7 @@ const PercentageSelectButtons = ({
<ButtonGroup
activeValue={sizePercentage}
onChange={(p) => handleSizePercentage(p)}
values={['10', '25', '50', '75', '100']}
values={values || DEFAULT_VALUES}
unit="%"
/>
</div>

View File

@ -509,9 +509,7 @@ const AdvancedTradeForm = () => {
</div>
{tradeForm.tradeType === 'Market' &&
selectedMarket instanceof Serum3Market ? (
<>
<SpotMarketOrderSwapForm />
</>
<SpotMarketOrderSwapForm />
) : (
<>
<form onSubmit={(e) => handleSubmit(e)}>

View File

@ -0,0 +1,61 @@
import MaxAmountButton from '@components/shared/MaxAmountButton'
import { useTokenMax } from '@components/swap/useTokenMax'
import mangoStore from '@store/mangoStore'
import useSelectedMarket from 'hooks/useSelectedMarket'
import { useTranslation } from 'next-i18next'
import { useCallback, useMemo } from 'react'
import { floorToDecimal, getDecimalCount } from 'utils/numbers'
const MaxMarketSwapAmount = ({
setAmountIn,
useMargin,
}: {
setAmountIn: (x: string) => void
useMargin: boolean
}) => {
const { t } = useTranslation('common')
const { price: oraclePrice, serumOrPerpMarket } = useSelectedMarket()
const mangoAccountLoading = mangoStore((s) => s.mangoAccount.initialLoad)
const {
amount: tokenMax,
amountWithBorrow,
decimals,
} = useTokenMax(useMargin)
const tickDecimals = useMemo(() => {
if (!serumOrPerpMarket) return decimals
return getDecimalCount(serumOrPerpMarket.tickSize)
}, [decimals, serumOrPerpMarket])
const maxAmount = useMemo(() => {
const balanceMax = useMargin ? amountWithBorrow : tokenMax
const { side } = mangoStore.getState().tradeForm
const sideMax =
side === 'buy' ? balanceMax.toNumber() / oraclePrice : balanceMax
return floorToDecimal(sideMax, tickDecimals).toFixed()
}, [amountWithBorrow, oraclePrice, tickDecimals, tokenMax, useMargin])
const setMax = useCallback(() => {
const { side } = mangoStore.getState().tradeForm
const max = useMargin ? amountWithBorrow : tokenMax
const maxDecimals = side === 'buy' ? tickDecimals : decimals
setAmountIn(floorToDecimal(max, maxDecimals).toFixed())
}, [decimals, setAmountIn, tickDecimals, useMargin])
if (mangoAccountLoading) return null
return (
<div className="mb-2 mt-3 flex items-center justify-between w-full">
<p className="text-xs text-th-fgd-3">{t('trade:size')}</p>
<MaxAmountButton
className="text-xs"
decimals={decimals}
label={t('max')}
onClick={() => setMax()}
value={maxAmount}
/>
</div>
)
}
export default MaxMarketSwapAmount

View File

@ -4,6 +4,7 @@ import NumberFormat, {
SourceInfo,
} from 'react-number-format'
import {
DEFAULT_CHECKBOX_SETTINGS,
INPUT_PREFIX_CLASSNAMES,
INPUT_SUFFIX_CLASSNAMES,
} from './AdvancedTradeForm'
@ -29,12 +30,16 @@ import * as sentry from '@sentry/nextjs'
import { isMangoError } from 'types'
import SwapSlider from '@components/swap/SwapSlider'
import PercentageSelectButtons from '@components/swap/PercentageSelectButtons'
import { SIZE_INPUT_UI_KEY } from 'utils/constants'
import { SIZE_INPUT_UI_KEY, TRADE_CHECKBOXES_KEY } from 'utils/constants'
import useLocalStorageState from 'hooks/useLocalStorageState'
import MaxSwapAmount from '@components/swap/MaxSwapAmount'
import useUnownedAccount from 'hooks/useUnownedAccount'
import HealthImpact from '@components/shared/HealthImpact'
import Tooltip from '@components/shared/Tooltip'
import Checkbox from '@components/forms/Checkbox'
import MaxMarketSwapAmount from './MaxMarketSwapAmount'
import { floorToDecimal, formatNumericValue } from 'utils/numbers'
import { formatTokenSymbol } from 'utils/tokens'
import FormatNumericValue from '@components/shared/FormatNumericValue'
const set = mangoStore.getState().set
const slippage = 100
@ -55,6 +60,8 @@ export default function SpotMarketOrderSwapForm() {
const { ipAllowed, ipCountry } = useIpAddress()
const { connected, publicKey, connect } = useWallet()
const [swapFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider')
const [savedCheckboxSettings, setSavedCheckboxSettings] =
useLocalStorageState(TRADE_CHECKBOXES_KEY, DEFAULT_CHECKBOX_SETTINGS)
const {
selectedMarket,
price: oraclePrice,
@ -274,28 +281,51 @@ export default function SpotMarketOrderSwapForm() {
: Math.trunc(simulatedHealthRatio)
}, [inputBank, outputBank, baseSize, quoteSize, side])
const [balance, borrowAmount] = useMemo(() => {
if (!inputBank) return [0, 0]
const mangoAccount = mangoStore.getState().mangoAccount.current
if (!mangoAccount) return [0, 0]
let borrowAmount
const balance = mangoAccount.getTokenDepositsUi(inputBank)
if (side === 'buy') {
const remainingBalance = balance - parseFloat(quoteSize)
borrowAmount = remainingBalance < 0 ? Math.abs(remainingBalance) : 0
} else {
const remainingBalance = balance - parseFloat(baseSize)
borrowAmount = remainingBalance < 0 ? Math.abs(remainingBalance) : 0
}
return [balance, borrowAmount]
}, [baseSize, inputBank, quoteSize])
const orderValue = useMemo(() => {
if (!outputBank || !oraclePrice || !baseSize || isNaN(parseFloat(baseSize)))
return 0
const basePriceDecimal = new Decimal(oraclePrice)
const quotePriceDecimal = new Decimal(outputBank.uiPrice)
const sizeDecimal = new Decimal(baseSize)
return floorToDecimal(
basePriceDecimal.mul(quotePriceDecimal).mul(sizeDecimal),
2,
)
}, [baseSize, outputBank, oraclePrice])
const disabled =
(connected && (!baseSize || !price)) ||
!serumOrPerpMarket ||
parseFloat(baseSize) < serumOrPerpMarket.minOrderSize ||
isLoading
const useMargin = true
return (
<>
<form onSubmit={(e) => handleSubmit(e)}>
<div className="mt-3 px-3 md:px-4">
<div className="mb-2 flex items-end justify-end">
{!isUnownedAccount ? (
<>
<MaxSwapAmount
useMargin={useMargin}
setAmountIn={setAmountFromSlider}
/>
</>
) : null}
</div>
{!isUnownedAccount ? (
<MaxMarketSwapAmount
useMargin={savedCheckboxSettings.margin}
setAmountIn={setAmountFromSlider}
/>
) : null}
<div className="flex flex-col">
<div className="relative">
<NumberFormat
@ -363,11 +393,9 @@ export default function SpotMarketOrderSwapForm() {
<div className={INPUT_SUFFIX_CLASSNAMES}>{quoteSymbol}</div>
</div>
</div>
</div>
<div className="mt-6 mb-4 flex px-3 md:px-4">
{swapFormSizeUi === 'slider' ? (
<SwapSlider
useMargin={useMargin}
useMargin={savedCheckboxSettings.margin}
amount={
side === 'buy'
? stringToNumberOrZero(quoteSize)
@ -380,106 +408,219 @@ export default function SpotMarketOrderSwapForm() {
<PercentageSelectButtons
amountIn={side === 'buy' ? quoteSize : baseSize}
setAmountIn={setAmountFromSlider}
useMargin={useMargin}
useMargin={savedCheckboxSettings.margin}
values={['25', '50', '75', '100']}
/>
)}
</div>
<div className="mt-6 mb-4 flex px-3 md:px-4">
{ipAllowed ? (
<Button
className={`flex w-full items-center justify-center ${
!connected
? ''
: side === 'buy'
? 'bg-th-up-dark text-white md:hover:bg-th-up-dark md:hover:brightness-90'
: 'bg-th-down-dark text-white md:hover:bg-th-down-dark md:hover:brightness-90'
}`}
disabled={disabled}
size="large"
type="submit"
>
{isLoading ? (
<div className="flex items-center space-x-2">
<Loading />
<span className="hidden sm:block">
{t('common:fetching-route')}
</span>
</div>
) : !connected ? (
<div className="flex items-center">
<LinkIcon className="mr-2 h-5 w-5" />
{t('connect')}
</div>
) : !placingOrder ? (
<span>
{t('trade:place-order', {
side: side === 'buy' ? t('buy') : t('sell'),
})}
</span>
) : (
<div className="flex items-center space-x-2">
<Loading />
<span className="hidden sm:block">
{t('trade:placing-order')}
</span>
</div>
)}
</Button>
) : (
<Button disabled className="w-full leading-tight" size="large">
{t('country-not-allowed', {
country: ipCountry ? `(${ipCountry})` : '',
})}
</Button>
)}
</div>
<div className="space-y-2 px-3 md:px-4">
<div className="">
<HealthImpact maintProjectedHealth={maintProjectedHealth} small />
</div>
<div className="flex justify-between text-xs">
<div className="mt-4">
<Tooltip
content={
<>
<p>
The price impact is the difference observed between the
total value of the entry tokens swapped and the destination
tokens obtained.
</p>
<p className="mt-1">
The bigger the trade is, the bigger the price impact can be.
</p>
</>
}
className="hidden md:block"
delay={100}
placement="left"
content={t('trade:tooltip-enable-margin')}
>
<p className="tooltip-underline">{t('swap:price-impact')}</p>
<Checkbox
checked={savedCheckboxSettings.margin}
onChange={(e) =>
setSavedCheckboxSettings({
...savedCheckboxSettings,
margin: e.target.checked,
})
}
>
{t('trade:margin')}
</Checkbox>
</Tooltip>
<p className="text-right font-mono text-th-fgd-2">
{selectedRoute
? selectedRoute?.priceImpactPct * 100 < 0.1
? '<0.1%'
: `${(selectedRoute?.priceImpactPct * 100).toFixed(2)}%`
: '-'}
</p>
</div>
<div className="flex items-center justify-between text-xs">
<p className="pr-2 text-th-fgd-3">{t('common:route')}</p>
<div className="flex items-center overflow-hidden text-th-fgd-3">
<div className="truncate whitespace-nowrap">
{selectedRoute?.marketInfos.map((info, index) => {
let includeSeparator = false
if (
selectedRoute?.marketInfos.length > 1 &&
index !== selectedRoute?.marketInfos.length - 1
) {
includeSeparator = true
}
return (
<span key={index}>{`${info?.label} ${
includeSeparator ? 'x ' : ''
}`}</span>
)
<div className="mt-6 mb-4 flex">
{ipAllowed ? (
<Button
className={`flex w-full items-center justify-center ${
!connected
? ''
: side === 'buy'
? 'bg-th-up-dark text-white md:hover:bg-th-up-dark md:hover:brightness-90'
: 'bg-th-down-dark text-white md:hover:bg-th-down-dark md:hover:brightness-90'
}`}
disabled={disabled}
size="large"
type="submit"
>
{isLoading ? (
<div className="flex items-center space-x-2">
<Loading />
<span className="hidden sm:block">
{t('common:fetching-route')}
</span>
</div>
) : !connected ? (
<div className="flex items-center">
<LinkIcon className="mr-2 h-5 w-5" />
{t('connect')}
</div>
) : !placingOrder ? (
<span>
{t('trade:place-order', {
side: side === 'buy' ? t('buy') : t('sell'),
})}
</span>
) : (
<div className="flex items-center space-x-2">
<Loading />
<span className="hidden sm:block">
{t('trade:placing-order')}
</span>
</div>
)}
</Button>
) : (
<Button disabled className="w-full leading-tight" size="large">
{t('country-not-allowed', {
country: ipCountry ? `(${ipCountry})` : '',
})}
</Button>
)}
</div>
<div className="space-y-2">
<div className="flex justify-between text-xs">
<p>{t('trade:order-value')}</p>
<p className="font-mono text-th-fgd-2">
{orderValue ? (
<FormatNumericValue value={orderValue} isUsd />
) : (
''
)}
</p>
</div>
<HealthImpact maintProjectedHealth={maintProjectedHealth} small />
<div className="flex justify-between text-xs">
<Tooltip
content={
<>
<p>
The price impact is the difference observed between the
total value of the entry tokens swapped and the
destination tokens obtained.
</p>
<p className="mt-1">
The bigger the trade is, the bigger the price impact can
be.
</p>
</>
}
>
<p className="tooltip-underline">{t('swap:price-impact')}</p>
</Tooltip>
<p className="text-right font-mono text-th-fgd-2">
{selectedRoute
? selectedRoute?.priceImpactPct * 100 < 0.1
? '<0.1%'
: `${(selectedRoute?.priceImpactPct * 100).toFixed(2)}%`
: '-'}
</p>
</div>
{borrowAmount && inputBank ? (
<>
<div className="flex justify-between text-xs">
<Tooltip
content={
balance
? t('trade:tooltip-borrow-balance', {
balance: formatNumericValue(balance),
borrowAmount: formatNumericValue(borrowAmount),
token: formatTokenSymbol(inputBank.name),
rate: formatNumericValue(
inputBank.getBorrowRateUi(),
2,
),
})
: t('trade:tooltip-borrow-no-balance', {
borrowAmount: formatNumericValue(borrowAmount),
token: formatTokenSymbol(inputBank.name),
rate: formatNumericValue(
inputBank.getBorrowRateUi(),
2,
),
})
}
delay={100}
>
<p className="tooltip-underline">{t('borrow-amount')}</p>
</Tooltip>
<p className="text-right font-mono text-th-fgd-2">
<FormatNumericValue
value={borrowAmount}
decimals={inputBank.mintDecimals}
/>{' '}
<span className="font-body text-th-fgd-4">
{formatTokenSymbol(inputBank.name)}
</span>
</p>
</div>
<div className="flex justify-between text-xs">
<Tooltip
content={t('loan-origination-fee-tooltip', {
fee: `${(
inputBank.loanOriginationFeeRate.toNumber() * 100
).toFixed(3)}%`,
})}
delay={100}
>
<p className="tooltip-underline">
{t('loan-origination-fee')}
</p>
</Tooltip>
<p className="text-right font-mono text-th-fgd-2">
<FormatNumericValue
value={
borrowAmount *
inputBank.loanOriginationFeeRate.toNumber()
}
decimals={inputBank.mintDecimals}
/>{' '}
<span className="font-body text-th-fgd-4">
{formatTokenSymbol(inputBank.name)}
</span>
</p>
</div>
</>
) : null}
<div className="flex items-center justify-between text-xs">
<p className="pr-2 text-th-fgd-3">{t('common:route')}</p>
<div className="flex items-center overflow-hidden text-th-fgd-2">
<Tooltip
content={selectedRoute?.marketInfos.map((info, index) => {
let includeSeparator = false
if (
selectedRoute?.marketInfos.length > 1 &&
index !== selectedRoute?.marketInfos.length - 1
) {
includeSeparator = true
}
return (
<span key={index}>{`${info?.label} ${
includeSeparator ? 'x ' : ''
}`}</span>
)
})}
>
<div className="tooltip-underline truncate whitespace-nowrap max-w-[140px]">
{selectedRoute?.marketInfos.map((info, index) => {
let includeSeparator = false
if (
selectedRoute?.marketInfos.length > 1 &&
index !== selectedRoute?.marketInfos.length - 1
) {
includeSeparator = true
}
return (
<span key={index}>{`${info?.label} ${
includeSeparator ? 'x ' : ''
}`}</span>
)
})}
</div>
</Tooltip>
</div>
</div>
</div>