add missing parts to market swap trade form
This commit is contained in:
parent
01f03022bf
commit
a151aa5190
|
@ -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>
|
||||
|
|
|
@ -509,9 +509,7 @@ const AdvancedTradeForm = () => {
|
|||
</div>
|
||||
{tradeForm.tradeType === 'Market' &&
|
||||
selectedMarket instanceof Serum3Market ? (
|
||||
<>
|
||||
<SpotMarketOrderSwapForm />
|
||||
</>
|
||||
<SpotMarketOrderSwapForm />
|
||||
) : (
|
||||
<>
|
||||
<form onSubmit={(e) => handleSubmit(e)}>
|
||||
|
|
|
@ -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
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue