simplify form

This commit is contained in:
saml33 2023-08-02 15:32:20 +10:00
parent 02301bedcc
commit 934e55ec93
11 changed files with 264 additions and 357 deletions

View File

@ -6,7 +6,7 @@ import {
Dispatch, Dispatch,
SetStateAction, SetStateAction,
} from 'react' } from 'react'
import { ArrowDownIcon } from '@heroicons/react/20/solid' import { ArrowDownIcon, ArrowsRightLeftIcon } from '@heroicons/react/20/solid'
import NumberFormat, { import NumberFormat, {
NumberFormatValues, NumberFormatValues,
SourceInfo, SourceInfo,
@ -18,7 +18,6 @@ import { SIZE_INPUT_UI_KEY } from '../../utils/constants'
import useLocalStorageState from 'hooks/useLocalStorageState' import useLocalStorageState from 'hooks/useLocalStorageState'
import SwapSlider from './SwapSlider' import SwapSlider from './SwapSlider'
import PercentageSelectButtons from './PercentageSelectButtons' import PercentageSelectButtons from './PercentageSelectButtons'
import Select from '@components/forms/Select'
import { floorToDecimal } from 'utils/numbers' import { floorToDecimal } from 'utils/numbers'
import { withValueLimit } from './MarketSwapForm' import { withValueLimit } from './MarketSwapForm'
import SellTokenInput from './SellTokenInput' import SellTokenInput from './SellTokenInput'
@ -26,36 +25,32 @@ import BuyTokenInput from './BuyTokenInput'
import { notify } from 'utils/notifications' import { notify } from 'utils/notifications'
import * as sentry from '@sentry/nextjs' import * as sentry from '@sentry/nextjs'
import { isMangoError } from 'types' import { isMangoError } from 'types'
import Button from '@components/shared/Button' import Button, { IconButton } from '@components/shared/Button'
import { useWallet } from '@solana/wallet-adapter-react'
import Loading from '@components/shared/Loading' import Loading from '@components/shared/Loading'
import TokenLogo from '@components/shared/TokenLogo' import TokenLogo from '@components/shared/TokenLogo'
import InlineNotification from '@components/shared/InlineNotification' import InlineNotification from '@components/shared/InlineNotification'
type LimitSwapFormProps = { type LimitSwapFormProps = {
showTokenSelect: 'input' | 'output' | undefined
setShowTokenSelect: Dispatch<SetStateAction<'input' | 'output' | undefined>> setShowTokenSelect: Dispatch<SetStateAction<'input' | 'output' | undefined>>
} }
type LimitSwapForm = { type LimitSwapForm = {
limitPrice: string | undefined amountIn: number
triggerPrice: string triggerPrice: string
} }
type FormErrors = Partial<Record<keyof LimitSwapForm, string>> type FormErrors = Partial<Record<keyof LimitSwapForm, string>>
const ORDER_TYPES = [
// 'trade:limit',
'trade:stop-market',
'trade:stop-limit',
]
const set = mangoStore.getState().set const set = mangoStore.getState().set
const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { const LimitSwapForm = ({
showTokenSelect,
setShowTokenSelect,
}: LimitSwapFormProps) => {
const { t } = useTranslation(['common', 'swap', 'trade']) const { t } = useTranslation(['common', 'swap', 'trade'])
const { connected } = useWallet()
const [animateSwitchArrow, setAnimateSwitchArrow] = useState(0) const [animateSwitchArrow, setAnimateSwitchArrow] = useState(0)
const [orderType, setOrderType] = useState(ORDER_TYPES[0])
const [triggerPrice, setTriggerPrice] = useState('') const [triggerPrice, setTriggerPrice] = useState('')
const [flipPrices, setFlipPrices] = useState(false)
const [submitting, setSubmitting] = useState(false) const [submitting, setSubmitting] = useState(false)
const [swapFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider') const [swapFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider')
const [formErrors, setFormErrors] = useState<FormErrors>({}) const [formErrors, setFormErrors] = useState<FormErrors>({})
@ -75,16 +70,6 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => {
: new Decimal(0) : new Decimal(0)
}, [amountInFormValue]) }, [amountInFormValue])
const [baseBank, quoteBank] = useMemo(() => {
if (inputBank && inputBank.name === 'USDC') {
return [outputBank, inputBank]
} else if (outputBank && outputBank.name === 'USDC') {
return [inputBank, outputBank]
} else if (inputBank && inputBank.name === 'SOL') {
return [outputBank, inputBank]
} else return [inputBank, outputBank]
}, [inputBank, outputBank])
const setAmountInFormValue = useCallback((amountIn: string) => { const setAmountInFormValue = useCallback((amountIn: string) => {
set((s) => { set((s) => {
s.swap.amountIn = amountIn s.swap.amountIn = amountIn
@ -103,88 +88,57 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => {
}) })
}, []) }, [])
const setLimitPrice = useCallback((price: string) => { const [quotePrice, flippedQuotePrice] = useMemo(() => {
set((s) => { if (!inputBank || !outputBank) return [0, 0]
s.swap.limitPrice = price const quote = floorToDecimal(
}) inputBank.uiPrice / outputBank.uiPrice,
}, []) outputBank.mintDecimals,
).toNumber()
const initialQuotePrice = useMemo(() => { const flipped = floorToDecimal(
if (!baseBank || !quoteBank) return outputBank.uiPrice / inputBank.uiPrice,
return floorToDecimal( inputBank.mintDecimals,
baseBank.uiPrice / quoteBank.uiPrice, ).toNumber()
quoteBank.mintDecimals, return [quote, flipped]
) }, [inputBank, outputBank])
}, [baseBank, quoteBank])
// set default limit and trigger price // set default limit and trigger price
useEffect(() => { useEffect(() => {
if (!initialQuotePrice) return if (!quotePrice) return
if (!triggerPrice) { if (!triggerPrice && !showTokenSelect) {
setTriggerPrice( setTriggerPrice(quotePrice.toFixed(outputBank?.mintDecimals))
initialQuotePrice.mul(0.9).toFixed(quoteBank?.mintDecimals),
)
} }
if (!limitPrice) { }, [quotePrice, outputBank, showTokenSelect, triggerPrice])
set((s) => {
s.swap.limitPrice = initialQuotePrice
.mul(0.8)
.toFixed(quoteBank?.mintDecimals)
})
}
}, [initialQuotePrice, limitPrice, quoteBank, triggerPrice])
const [limitPriceDifference, triggerPriceDifference] = useMemo(() => { const triggerPriceDifference = useMemo(() => {
if (!initialQuotePrice) return [0, 0] if ((!flipPrices && !quotePrice) || (flipPrices && !flippedQuotePrice))
const initialPrice = initialQuotePrice.toNumber() return 0
const limitDifference = limitPrice const oraclePrice = !flipPrices ? quotePrice : flippedQuotePrice
? ((parseFloat(limitPrice) - initialPrice) / initialPrice) * 100
: 0
const triggerDifference = triggerPrice const triggerDifference = triggerPrice
? ((parseFloat(triggerPrice) - initialPrice) / initialPrice) * 100 ? ((parseFloat(triggerPrice) - oraclePrice) / oraclePrice) * 100
: 0 : 0
return [limitDifference, triggerDifference] return triggerDifference
}, [initialQuotePrice, limitPrice, triggerPrice]) }, [flippedQuotePrice, quotePrice, triggerPrice])
// const isFormValid = useCallback( const handleTokenSelect = (type: 'input' | 'output') => {
// (form: LimitSwapForm) => { setShowTokenSelect(type)
// const invalidFields: FormErrors = {} setTriggerPrice('')
// setFormErrors({}) }
// const triggerPriceNumber = parseFloat(form.triggerPrice)
// const requiredFields: (keyof LimitSwapForm)[] = [ const isFormValid = useCallback((form: LimitSwapForm) => {
// 'limitPrice', const invalidFields: FormErrors = {}
// 'triggerPrice', setFormErrors({})
// ] const requiredFields: (keyof LimitSwapForm)[] = ['amountIn', 'triggerPrice']
// for (const key of requiredFields) { for (const key of requiredFields) {
// const value = form[key] as string const value = form[key] as string
// if (!value) { if (!value) {
// if (orderType === 'trade:stop-market') { invalidFields[key] = t('settings:error-required-field')
// if (key !== 'limitPrice') { }
// invalidFields[key] = t('settings:error-required-field') }
// } if (Object.keys(invalidFields).length) {
// } else { setFormErrors(invalidFields)
// invalidFields[key] = t('settings:error-required-field') }
// } return invalidFields
// } }, [])
// }
// if (
// orderType.includes('stop') &&
// initialQuotePrice &&
// triggerPriceNumber > initialQuotePrice.toNumber()
// ) {
// invalidFields.triggerPrice =
// 'Trigger price must be less than current price'
// }
// if (form.limitPrice && form.limitPrice > form.triggerPrice) {
// invalidFields.limitPrice = 'Limit price must be less than trigger price'
// }
// if (Object.keys(invalidFields).length) {
// setFormErrors(invalidFields)
// }
// return invalidFields
// },
// [initialQuotePrice, orderType],
// )
/* /*
If the use margin setting is toggled, clear the form values If the use margin setting is toggled, clear the form values
@ -194,16 +148,10 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => {
setAmountOutFormValue('') setAmountOutFormValue('')
}, [useMargin, setAmountInFormValue, setAmountOutFormValue]) }, [useMargin, setAmountInFormValue, setAmountOutFormValue])
// the price to use for calculating the opposing side's size
const sizePrice = useMemo(() => {
return orderType === 'trade:stop-market' ? triggerPrice : limitPrice
}, [limitPrice, orderType, triggerPrice])
// get the out amount from the in amount and trigger or limit price // get the out amount from the in amount and trigger or limit price
const getAmountOut = useCallback( const getAmountOut = useCallback(
(amountIn: string, price: string) => { (amountIn: string, price: string) => {
const amountOut = const amountOut = !flipPrices
outputBank?.name === quoteBank?.name
? floorToDecimal( ? floorToDecimal(
parseFloat(amountIn) * parseFloat(price), parseFloat(amountIn) * parseFloat(price),
outputBank?.mintDecimals || 0, outputBank?.mintDecimals || 0,
@ -214,14 +162,13 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => {
) )
return amountOut return amountOut
}, },
[outputBank, quoteBank], [outputBank, flipPrices],
) )
// get the in amount from the out amount and trigger or limit price // get the in amount from the out amount and trigger or limit price
const getAmountIn = useCallback( const getAmountIn = useCallback(
(amountOut: string, price: string) => { (amountOut: string, price: string) => {
const amountIn = const amountIn = !flipPrices
outputBank?.name === quoteBank?.name
? floorToDecimal( ? floorToDecimal(
parseFloat(amountOut) / parseFloat(price), parseFloat(amountOut) / parseFloat(price),
inputBank?.mintDecimals || 0, inputBank?.mintDecimals || 0,
@ -232,77 +179,66 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => {
) )
return amountIn return amountIn
}, },
[inputBank, outputBank, quoteBank], [inputBank, outputBank, flipPrices],
) )
const handleMax = useCallback( const handleMax = useCallback(
(amountIn: string) => { (amountIn: string) => {
setAmountInFormValue(amountIn) setAmountInFormValue(amountIn)
if (parseFloat(amountIn) > 0 && sizePrice) { if (parseFloat(amountIn) > 0 && triggerPrice) {
const amountOut = getAmountOut(amountIn, sizePrice) const amountOut = getAmountOut(amountIn, triggerPrice)
setAmountOutFormValue(amountOut.toString()) setAmountOutFormValue(amountOut.toString())
} }
}, },
[getAmountOut, setAmountInFormValue, setAmountOutFormValue, sizePrice], [getAmountOut, setAmountInFormValue, setAmountOutFormValue, triggerPrice],
) )
const handleRepay = useCallback( const handleRepay = useCallback(
(amountOut: string) => { (amountOut: string) => {
setAmountOutFormValue(amountOut) setAmountOutFormValue(amountOut)
if (parseFloat(amountOut) > 0 && sizePrice) { if (parseFloat(amountOut) > 0 && triggerPrice) {
const amountIn = getAmountIn(amountOut, sizePrice) const amountIn = getAmountIn(amountOut, triggerPrice)
setAmountInFormValue(amountIn.toString()) setAmountInFormValue(amountIn.toString())
} }
}, },
[getAmountIn, setAmountInFormValue, setAmountOutFormValue, sizePrice], [getAmountIn, setAmountInFormValue, setAmountOutFormValue, triggerPrice],
) )
const handleAmountInChange = useCallback( const handleAmountInChange = useCallback(
(e: NumberFormatValues, info: SourceInfo) => { (e: NumberFormatValues, info: SourceInfo) => {
if (info.source !== 'event') return if (info.source !== 'event') return
setFormErrors({})
setAmountInFormValue(e.value) setAmountInFormValue(e.value)
if (parseFloat(e.value) > 0 && sizePrice) { if (parseFloat(e.value) > 0 && triggerPrice) {
const amountOut = getAmountOut(e.value, sizePrice) const amountOut = getAmountOut(e.value, triggerPrice)
setAmountOutFormValue(amountOut.toString()) setAmountOutFormValue(amountOut.toString())
} }
}, },
[getAmountOut, setAmountInFormValue, setAmountOutFormValue, sizePrice], [getAmountOut, setAmountInFormValue, setAmountOutFormValue, triggerPrice],
) )
const handleAmountOutChange = useCallback( const handleAmountOutChange = useCallback(
(e: NumberFormatValues, info: SourceInfo) => { (e: NumberFormatValues, info: SourceInfo) => {
if (info.source !== 'event') return if (info.source !== 'event') return
setAmountOutFormValue(e.value) setAmountOutFormValue(e.value)
if (parseFloat(e.value) > 0 && sizePrice) { if (parseFloat(e.value) > 0 && triggerPrice) {
const amountIn = getAmountIn(e.value, sizePrice) const amountIn = getAmountIn(e.value, triggerPrice)
setAmountInFormValue(amountIn.toString()) setAmountInFormValue(amountIn.toString())
} }
}, },
[getAmountIn, setAmountInFormValue, setAmountOutFormValue, sizePrice], [getAmountIn, setAmountInFormValue, setAmountOutFormValue, triggerPrice],
) )
const handleAmountInUi = useCallback( const handleAmountInUi = useCallback(
(amountIn: string) => { (amountIn: string) => {
setAmountInFormValue(amountIn) setAmountInFormValue(amountIn)
if (sizePrice) {
const amountOut = getAmountOut(amountIn, sizePrice)
setAmountOutFormValue(amountOut.toString())
}
},
[getAmountOut, setAmountInFormValue, setAmountOutFormValue, sizePrice],
)
const handleLimitPrice = useCallback(
(e: NumberFormatValues, info: SourceInfo) => {
if (info.source !== 'event') return
setFormErrors({}) setFormErrors({})
setLimitPrice(e.value) if (triggerPrice) {
if (parseFloat(e.value) > 0 && parseFloat(amountInFormValue) > 0) { const amountOut = getAmountOut(amountIn, triggerPrice)
const amountOut = getAmountOut(amountInFormValue, e.value)
setAmountOutFormValue(amountOut.toString()) setAmountOutFormValue(amountOut.toString())
} }
}, },
[amountInFormValue, setLimitPrice], [getAmountOut, setAmountInFormValue, setAmountOutFormValue, triggerPrice],
) )
const handleTriggerPrice = useCallback( const handleTriggerPrice = useCallback(
@ -310,49 +246,48 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => {
if (info.source !== 'event') return if (info.source !== 'event') return
setFormErrors({}) setFormErrors({})
setTriggerPrice(e.value) setTriggerPrice(e.value)
if ( if (parseFloat(e.value) > 0 && parseFloat(amountInFormValue) > 0) {
parseFloat(e.value) > 0 &&
parseFloat(amountInFormValue) > 0 &&
orderType === 'trade:stop-market'
) {
const amountOut = getAmountOut(amountInFormValue, e.value) const amountOut = getAmountOut(amountInFormValue, e.value)
setAmountOutFormValue(amountOut.toString()) setAmountOutFormValue(amountOut.toString())
} }
}, },
[amountInFormValue, orderType, setTriggerPrice], [amountInFormValue, flipPrices, setTriggerPrice],
) )
const handleSwitchTokens = useCallback(() => { const handleSwitchTokens = useCallback(() => {
if (amountInAsDecimal?.gt(0) && sizePrice) { if (amountInAsDecimal?.gt(0) && triggerPrice) {
const amountOut = const amountOut = amountInAsDecimal.div(triggerPrice)
outputBank?.name !== quoteBank?.name
? amountInAsDecimal.mul(sizePrice)
: amountInAsDecimal.div(sizePrice)
setAmountOutFormValue(amountOut.toString()) setAmountOutFormValue(amountOut.toString())
} }
set((s) => { set((s) => {
s.swap.inputBank = outputBank s.swap.inputBank = outputBank
s.swap.outputBank = inputBank s.swap.outputBank = inputBank
// s.swap.limitPrice = ''
}) })
if (flippedQuotePrice) {
setTriggerPrice(flippedQuotePrice.toFixed(inputBank?.mintDecimals))
}
setAnimateSwitchArrow( setAnimateSwitchArrow(
(prevanimateSwitchArrow) => prevanimateSwitchArrow + 1, (prevanimateSwitchArrow) => prevanimateSwitchArrow + 1,
) )
}, [ }, [
setAmountInFormValue, setAmountInFormValue,
amountInAsDecimal, amountInAsDecimal,
flipPrices,
flippedQuotePrice,
inputBank, inputBank,
orderType,
outputBank, outputBank,
quoteBank, triggerPrice,
sizePrice,
]) ])
const handlePlaceStopLoss = useCallback(async () => { const handlePlaceStopLoss = useCallback(async () => {
// const invalidFields = isFormValid({ limitPrice, triggerPrice }) const invalidFields = isFormValid({
// if (Object.keys(invalidFields).length) { amountIn: amountInAsDecimal.toNumber(),
// return triggerPrice,
// } })
if (Object.keys(invalidFields).length) {
return
}
try { try {
const client = mangoStore.getState().client const client = mangoStore.getState().client
const group = mangoStore.getState().group const group = mangoStore.getState().group
@ -361,34 +296,25 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => {
const inputBank = mangoStore.getState().swap.inputBank const inputBank = mangoStore.getState().swap.inputBank
const outputBank = mangoStore.getState().swap.outputBank const outputBank = mangoStore.getState().swap.outputBank
if ( if (!mangoAccount || !group || !inputBank || !outputBank || !triggerPrice)
!mangoAccount ||
!group ||
!inputBank ||
!outputBank ||
(!triggerPrice && orderType !== 'trade:limit') ||
(!limitPrice && orderType !== 'trade:stop-market')
)
return return
setSubmitting(true) setSubmitting(true)
const orderPrice = const inputMint = !flipPrices ? inputBank.mint : outputBank.mint
orderType === 'trade:limit' const outputMint = !flipPrices ? outputBank.mint : inputBank.mint
? parseFloat(limitPrice!) const amountIn = !flipPrices
: parseFloat(triggerPrice) ? amountInAsDecimal.toNumber()
: parseFloat(amountOutFormValue)
const stopLimitPrice =
orderType !== 'trade:stop-market' ? parseFloat(limitPrice!) : 0
try { try {
const tx = await client.tokenConditionalSwapStopLoss( const tx = await client.tokenConditionalSwapStopLoss(
group, group,
mangoAccount, mangoAccount,
inputBank.mint, inputMint,
orderPrice, parseFloat(triggerPrice),
outputBank.mint, outputMint,
stopLimitPrice, null,
amountInAsDecimal.toNumber(), amountIn,
null, null,
null, null,
) )
@ -417,44 +343,57 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => {
} finally { } finally {
setSubmitting(false) setSubmitting(false)
} }
}, [orderType, limitPrice, triggerPrice, amountInAsDecimal]) }, [
flipPrices,
limitPrice,
triggerPrice,
amountInAsDecimal,
amountOutFormValue,
])
const limitOrderDisabled = const triggerPriceLabel = useMemo(() => {
!connected || !amountInFormValue || !amountOutFormValue if (!inputBank || !outputBank) return t('trade:trigger-price')
if (inputBank.name === 'USDC') {
return t('trade:trigger-order-rate', {
side: t('buy').toLowerCase(),
symbol: outputBank.name,
})
} else {
return t('trade:trigger-order-rate', {
side: t('sell').toLowerCase(),
symbol: inputBank.name,
})
}
}, [inputBank, outputBank])
const handleFlipPrices = useCallback(
(flip: boolean) => {
setFlipPrices(flip)
if (flip) {
setTriggerPrice(flippedQuotePrice.toFixed(inputBank?.mintDecimals))
} else {
setTriggerPrice(quotePrice.toFixed(outputBank?.mintDecimals))
}
},
[flippedQuotePrice, inputBank, outputBank, quotePrice],
)
return ( return (
<> <>
<SellTokenInput <SellTokenInput
className="rounded-b-none" className="rounded-b-none"
error={formErrors.amountIn}
handleAmountInChange={handleAmountInChange} handleAmountInChange={handleAmountInChange}
setShowTokenSelect={setShowTokenSelect} setShowTokenSelect={() => handleTokenSelect('input')}
handleMax={handleMax} handleMax={handleMax}
/> />
<div <div
className={`grid ${ className={`grid grid-cols-2 gap-2 rounded-b-xl bg-th-bkg-2 p-3 pt-1`}
orderType === 'trade:stop-limit' ? 'grid-cols-3' : 'grid-cols-2'
} gap-2 rounded-b-xl bg-th-bkg-2 p-3 pt-1`}
id="swap-step-two" id="swap-step-two"
> >
<div className="col-span-1"> <div className="col-span-2">
<p className="mb-2 text-th-fgd-2">{t('trade:order-type')}</p>
<Select
value={t(orderType)}
onChange={(type) => setOrderType(type)}
className="w-full"
buttonClassName="ring-transparent rounded-t-lg rounded-b-lg focus:outline-none md:hover:bg-th-bkg-1 md:hover:ring-transparent focus-visible:bg-th-bkg-3 whitespace-nowrap"
>
{ORDER_TYPES.map((type) => (
<Select.Option key={type} value={type}>
{t(type)}
</Select.Option>
))}
</Select>
</div>
{orderType !== 'trade:limit' ? (
<div className="col-span-1">
<p className="mb-2 text-th-fgd-2"> <p className="mb-2 text-th-fgd-2">
{t('trade:trigger-price')}{' '} {triggerPriceLabel}{' '}
<span className="text-xs text-th-fgd-3"> <span className="text-xs text-th-fgd-3">
{triggerPriceDifference {triggerPriceDifference
? `(${triggerPriceDifference.toFixed(2)}%)` ? `(${triggerPriceDifference.toFixed(2)}%)`
@ -477,7 +416,12 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => {
isAllowed={withValueLimit} isAllowed={withValueLimit}
/> />
<div className="absolute top-1/2 -translate-y-1/2 left-2"> <div className="absolute top-1/2 -translate-y-1/2 left-2">
<TokenLogo bank={quoteBank} size={16} /> <TokenLogo bank={flipPrices ? inputBank : outputBank} size={16} />
</div>
<div className="absolute top-1/2 -translate-y-1/2 right-2">
<IconButton hideBg onClick={() => handleFlipPrices(!flipPrices)}>
<ArrowsRightLeftIcon className="h-4 w-4" />
</IconButton>
</div> </div>
</div> </div>
{formErrors.triggerPrice ? ( {formErrors.triggerPrice ? (
@ -491,48 +435,6 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => {
</div> </div>
) : null} ) : null}
</div> </div>
) : null}
{orderType !== 'trade:stop-market' ? (
<div className="col-span-1">
<p className="mb-2 text-th-fgd-2">
{t('trade:limit-price')}{' '}
<span className="text-xs text-th-fgd-3">
{limitPriceDifference
? `(${limitPriceDifference.toFixed(2)}%)`
: ''}
</span>
</p>
<div className="relative">
<NumberFormat
inputMode="decimal"
thousandSeparator=","
allowNegative={false}
isNumericString={true}
decimalScale={outputBank?.mintDecimals || 6}
name="limitPrice"
id="limitPrice"
className="h-10 w-full rounded-lg bg-th-input-bkg p-3 pl-8 font-mono text-sm text-th-fgd-1 focus:outline-none md:hover:bg-th-bkg-1"
placeholder="0.00"
value={limitPrice}
onValueChange={handleLimitPrice}
isAllowed={withValueLimit}
/>
<div className="absolute top-1/2 -translate-y-1/2 left-2">
<TokenLogo bank={quoteBank} size={16} />
</div>
</div>
{formErrors.limitPrice ? (
<div className="mt-1">
<InlineNotification
type="error"
desc={formErrors.limitPrice}
hideBorder
hidePadding
/>
</div>
) : null}
</div>
) : null}
</div> </div>
<div className="my-2 flex justify-center"> <div className="my-2 flex justify-center">
<button <button
@ -551,7 +453,7 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => {
</div> </div>
<BuyTokenInput <BuyTokenInput
handleAmountOutChange={handleAmountOutChange} handleAmountOutChange={handleAmountOutChange}
setShowTokenSelect={setShowTokenSelect} setShowTokenSelect={() => handleTokenSelect('output')}
handleRepay={handleRepay} handleRepay={handleRepay}
/> />
{swapFormSizeUi === 'slider' ? ( {swapFormSizeUi === 'slider' ? (
@ -571,7 +473,6 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => {
<Button <Button
onClick={handlePlaceStopLoss} onClick={handlePlaceStopLoss}
className="mt-6 mb-4 flex w-full items-center justify-center text-base" className="mt-6 mb-4 flex w-full items-center justify-center text-base"
disabled={limitOrderDisabled}
size="large" size="large"
> >
{submitting ? <Loading /> : t('swap:place-limit-order')} {submitting ? <Loading /> : t('swap:place-limit-order')}

View File

@ -12,17 +12,20 @@ import { INPUT_TOKEN_DEFAULT } from 'utils/constants'
import { NUMBER_FORMAT_CLASSNAMES, withValueLimit } from './MarketSwapForm' import { NUMBER_FORMAT_CLASSNAMES, withValueLimit } from './MarketSwapForm'
import MaxSwapAmount from './MaxSwapAmount' import MaxSwapAmount from './MaxSwapAmount'
import useUnownedAccount from 'hooks/useUnownedAccount' import useUnownedAccount from 'hooks/useUnownedAccount'
import InlineNotification from '@components/shared/InlineNotification'
const SellTokenInput = ({ const SellTokenInput = ({
handleAmountInChange, handleAmountInChange,
setShowTokenSelect, setShowTokenSelect,
handleMax, handleMax,
className, className,
error,
}: { }: {
handleAmountInChange: (e: NumberFormatValues, info: SourceInfo) => void handleAmountInChange: (e: NumberFormatValues, info: SourceInfo) => void
setShowTokenSelect: Dispatch<SetStateAction<'input' | 'output' | undefined>> setShowTokenSelect: Dispatch<SetStateAction<'input' | 'output' | undefined>>
handleMax: (amountIn: string) => void handleMax: (amountIn: string) => void
className?: string className?: string
error?: string
}) => { }) => {
const { t } = useTranslation('common') const { t } = useTranslation('common')
const { group } = useMangoGroup() const { group } = useMangoGroup()
@ -74,6 +77,16 @@ const SellTokenInput = ({
: ''} : ''}
</span> </span>
</div> </div>
{error ? (
<div className="col-span-2 mt-1 flex justify-end">
<InlineNotification
type="error"
desc={error}
hideBorder
hidePadding
/>
</div>
) : null}
</div> </div>
) )
} }

View File

@ -233,7 +233,7 @@ const SwapForm = () => {
<div className="mb-6"> <div className="mb-6">
<TabUnderline <TabUnderline
activeValue={swapOrLimit} activeValue={swapOrLimit}
values={['swap', 'trade:stop-loss']} values={['swap', 'trade:trigger-order']}
onChange={(v) => handleSwapOrLimit(v)} onChange={(v) => handleSwapOrLimit(v)}
/> />
</div> </div>
@ -254,7 +254,10 @@ const SwapForm = () => {
setShowTokenSelect={setShowTokenSelect} setShowTokenSelect={setShowTokenSelect}
/> />
) : ( ) : (
<LimitSwapForm setShowTokenSelect={setShowTokenSelect} /> <LimitSwapForm
showTokenSelect={showTokenSelect}
setShowTokenSelect={setShowTokenSelect}
/>
)} )}
{ipAllowed ? ( {ipAllowed ? (
swapOrLimit === 'swap' ? ( swapOrLimit === 'swap' ? (

View File

@ -20,7 +20,7 @@ const SwapInfoTabs = () => {
?.length || 0 ?.length || 0
return [ return [
['balances', 0], ['balances', 0],
['trade:stop-orders', stopOrdersCount], ['trade:trigger-orders', stopOrdersCount],
['swap:swap-history', 0], ['swap:swap-history', 0],
] ]
}, [mangoAccount]) }, [mangoAccount])
@ -41,7 +41,7 @@ const SwapInfoTabs = () => {
/> />
</div> </div>
{selectedTab === 'balances' ? <SwapTradeBalances /> : null} {selectedTab === 'balances' ? <SwapTradeBalances /> : null}
{selectedTab === 'trade:stop-orders' ? <SwapOrders /> : null} {selectedTab === 'trade:trigger-orders' ? <SwapOrders /> : null}
{selectedTab === 'swap:swap-history' ? <SwapHistoryTable /> : null} {selectedTab === 'swap:swap-history' ? <SwapHistoryTable /> : null}
</div> </div>
) )

View File

@ -51,23 +51,21 @@ const SwapOrders = () => {
sellBank.mintDecimals, sellBank.mintDecimals,
).toNumber() ).toNumber()
const triggerPrice = order.getPriceLowerLimitUi(group) const triggerPrice = order.getPriceLowerLimitUi(group)
const limitPrice = order.getPriceUpperLimitUi(group)
const pricePremium = order.getPricePremium() const pricePremium = order.getPricePremium()
const filled = order.getSoldUi(group) const filled = order.getSoldUi(group)
const currentPrice = (sellBank.uiPrice / buyBank.uiPrice).toFixed(
const orderType = buyBank.mintDecimals,
limitPrice === 0 ? 'trade:stop-market' : 'trade:stop-limit' )
const data = { const data = {
...order, ...order,
buyBank, buyBank,
currentPrice,
sellBank, sellBank,
pair, pair,
size, size,
filled, filled,
triggerPrice, triggerPrice,
limitPrice,
orderType,
fee: pricePremium, fee: pricePremium,
} }
formatted.push(data) formatted.push(data)
@ -136,16 +134,6 @@ const SwapOrders = () => {
title={t('swap:pair')} title={t('swap:pair')}
/> />
</Th> </Th>
<Th>
<div className="flex justify-end">
<SortableColumnHeader
sortKey="orderType"
sort={() => requestSort('orderType')}
sortConfig={sortConfig}
title={t('trade:order-type')}
/>
</div>
</Th>
<Th> <Th>
<div className="flex justify-end"> <div className="flex justify-end">
<SortableColumnHeader <SortableColumnHeader
@ -169,20 +157,20 @@ const SwapOrders = () => {
<Th> <Th>
<div className="flex justify-end"> <div className="flex justify-end">
<SortableColumnHeader <SortableColumnHeader
sortKey="triggerPrice" sortKey="currentPrice"
sort={() => requestSort('triggerPrice')} sort={() => requestSort('currentPrice')}
sortConfig={sortConfig} sortConfig={sortConfig}
title={t('trade:trigger-price')} title={t('trade:current-price')}
/> />
</div> </div>
</Th> </Th>
<Th> <Th>
<div className="flex justify-end"> <div className="flex justify-end">
<SortableColumnHeader <SortableColumnHeader
sortKey="limitPrice" sortKey="triggerPrice"
sort={() => requestSort('limitPrice')} sort={() => requestSort('triggerPrice')}
sortConfig={sortConfig} sortConfig={sortConfig}
title={t('trade:limit-price')} title={t('trade:trigger-price')}
/> />
</div> </div>
</Th> </Th>
@ -203,10 +191,9 @@ const SwapOrders = () => {
{tableData.map((data, i) => { {tableData.map((data, i) => {
const { const {
buyBank, buyBank,
currentPrice,
fee, fee,
pair, pair,
orderType,
limitPrice,
sellBank, sellBank,
size, size,
filled, filled,
@ -215,9 +202,6 @@ const SwapOrders = () => {
return ( return (
<TrBody key={i} className="text-sm"> <TrBody key={i} className="text-sm">
<Td>{pair}</Td> <Td>{pair}</Td>
<Td>
<p className="text-right font-body">{t(orderType)}</p>
</Td>
<Td> <Td>
<p className="text-right"> <p className="text-right">
{size} {size}
@ -237,7 +221,15 @@ const SwapOrders = () => {
</p> </p>
</Td> </Td>
<Td> <Td>
{triggerPrice ? ( <p className="text-right">
{currentPrice}
<span className="text-th-fgd-3 font-body">
{' '}
{buyBank.name}
</span>
</p>
</Td>
<Td>
<p className="text-right"> <p className="text-right">
{triggerPrice} {triggerPrice}
<span className="text-th-fgd-3 font-body"> <span className="text-th-fgd-3 font-body">
@ -245,28 +237,16 @@ const SwapOrders = () => {
{buyBank.name} {buyBank.name}
</span> </span>
</p> </p>
) : (
<p className="text-right"></p>
)}
</Td>
<Td>
{limitPrice ? (
<p className="text-right">
{limitPrice}
<span className="text-th-fgd-3 font-body">
{' '}
{buyBank.name}
</span>
</p>
) : (
<p className="text-right"></p>
)}
</Td> </Td>
<Td> <Td>
<p className="text-right">{fee.toFixed(2)}%</p> <p className="text-right">{fee.toFixed(2)}%</p>
</Td> </Td>
<Td className="flex justify-end"> <Td className="flex justify-end">
<IconButton onClick={() => handleCancel(data.id)} size="small"> <IconButton
disabled={cancelId === data.id.toString()}
onClick={() => handleCancel(data.id)}
size="small"
>
{cancelId === data.id.toString() ? ( {cancelId === data.id.toString() ? (
<Loading /> <Loading />
) : ( ) : (

View File

@ -34,10 +34,10 @@ const SwapPage = () => {
return ( return (
<> <>
<div className="grid grid-cols-12"> <div className="grid grid-cols-12">
<div className="col-span-12 border-th-bkg-3 md:col-span-6 md:border-b lg:col-span-7 2xl:col-span-8"> <div className="col-span-12 border-th-bkg-3 md:col-span-6 md:border-b lg:col-span-7 xl:col-span-8">
<SwapTokenChart /> <SwapTokenChart />
</div> </div>
<div className="col-span-12 mt-2 space-y-6 border-th-bkg-3 md:col-span-6 md:mt-0 md:border-b lg:col-span-5 2xl:col-span-4"> <div className="col-span-12 mt-2 space-y-6 border-th-bkg-3 md:col-span-6 md:mt-0 md:border-b lg:col-span-5 xl:col-span-4">
<SwapForm /> <SwapForm />
</div> </div>
<div className="col-span-12"> <div className="col-span-12">

View File

@ -86,7 +86,6 @@
"stop-limit": "Stop Limit", "stop-limit": "Stop Limit",
"stop-loss": "Stop-loss", "stop-loss": "Stop-loss",
"stop-market": "Stop Market", "stop-market": "Stop Market",
"stop-orders": "Stop Orders",
"taker": "Taker", "taker": "Taker",
"taker-fee": "Taker Fee", "taker-fee": "Taker Fee",
"tick-size": "Tick Size", "tick-size": "Tick Size",
@ -104,6 +103,9 @@
"trade-sounds-tooltip": "Play a sound alert for every new trade", "trade-sounds-tooltip": "Play a sound alert for every new trade",
"trades": "Trades", "trades": "Trades",
"trigger-price": "Trigger Price", "trigger-price": "Trigger Price",
"trigger-order": "Trigger Order",
"trigger-order-rate": "Trigger {{side}} {{symbol}} price",
"trigger-orders": "Trigger Orders",
"tweet-position": "Tweet", "tweet-position": "Tweet",
"unrealized-pnl": "Unrealized PnL", "unrealized-pnl": "Unrealized PnL",
"unsettled": "Unsettled", "unsettled": "Unsettled",

View File

@ -86,7 +86,6 @@
"stop-limit": "Stop Limit", "stop-limit": "Stop Limit",
"stop-loss": "Stop-loss", "stop-loss": "Stop-loss",
"stop-market": "Stop Market", "stop-market": "Stop Market",
"stop-orders": "Stop Orders",
"taker": "Taker", "taker": "Taker",
"taker-fee": "Taker Fee", "taker-fee": "Taker Fee",
"tick-size": "Tick Size", "tick-size": "Tick Size",
@ -104,6 +103,9 @@
"trade-sounds-tooltip": "Play a sound alert for every new trade", "trade-sounds-tooltip": "Play a sound alert for every new trade",
"trades": "Trades", "trades": "Trades",
"trigger-price": "Trigger Price", "trigger-price": "Trigger Price",
"trigger-order": "Trigger Order",
"trigger-order-rate": "Trigger {{side}} {{symbol}} price",
"trigger-orders": "Trigger Orders",
"tweet-position": "Tweet", "tweet-position": "Tweet",
"unrealized-pnl": "Unrealized PnL", "unrealized-pnl": "Unrealized PnL",
"unsettled": "Unsettled", "unsettled": "Unsettled",

View File

@ -86,7 +86,6 @@
"stop-limit": "Stop Limit", "stop-limit": "Stop Limit",
"stop-loss": "Stop-loss", "stop-loss": "Stop-loss",
"stop-market": "Stop Market", "stop-market": "Stop Market",
"stop-orders": "Stop Orders",
"taker": "Taker", "taker": "Taker",
"taker-fee": "Taker Fee", "taker-fee": "Taker Fee",
"tick-size": "Tick Size", "tick-size": "Tick Size",
@ -104,6 +103,9 @@
"trade-sounds-tooltip": "Play a sound alert for every new trade", "trade-sounds-tooltip": "Play a sound alert for every new trade",
"trades": "Trades", "trades": "Trades",
"trigger-price": "Trigger Price", "trigger-price": "Trigger Price",
"trigger-order": "Trigger Order",
"trigger-order-rate": "Trigger {{side}} {{symbol}} price",
"trigger-orders": "Trigger Orders",
"tweet-position": "Tweet", "tweet-position": "Tweet",
"unrealized-pnl": "Unrealized PnL", "unrealized-pnl": "Unrealized PnL",
"unsettled": "Unsettled", "unsettled": "Unsettled",

View File

@ -85,7 +85,6 @@
"stop-limit": "Stop Limit", "stop-limit": "Stop Limit",
"stop-loss": "Stop-loss", "stop-loss": "Stop-loss",
"stop-market": "Stop Market", "stop-market": "Stop Market",
"stop-orders": "Stop Orders",
"taker": "吃單者", "taker": "吃單者",
"tick-size": "波動單位", "tick-size": "波動單位",
"taker-fee": "Taker Fee", "taker-fee": "Taker Fee",
@ -102,6 +101,9 @@
"trades": "交易", "trades": "交易",
"tweet-position": "分享至Twitter", "tweet-position": "分享至Twitter",
"trigger-price": "Trigger Price", "trigger-price": "Trigger Price",
"trigger-order": "Trigger Order",
"trigger-order-rate": "Trigger {{side}} {{symbol}} price",
"trigger-orders": "Trigger Orders",
"unsettled": "未結清", "unsettled": "未結清",
"volume-alert": "交易量警報", "volume-alert": "交易量警報",
"volume-alert-desc": "交易量超過警報設定時播放聲音" "volume-alert-desc": "交易量超過警報設定時播放聲音"

View File

@ -86,7 +86,6 @@
"stop-limit": "Stop Limit", "stop-limit": "Stop Limit",
"stop-loss": "Stop-loss", "stop-loss": "Stop-loss",
"stop-market": "Stop Market", "stop-market": "Stop Market",
"stop-orders": "Stop Orders",
"taker": "吃單者", "taker": "吃單者",
"taker-fee": "吃單者費用", "taker-fee": "吃單者費用",
"tick-size": "波動單位", "tick-size": "波動單位",
@ -104,6 +103,9 @@
"trade-sounds-tooltip": "為每筆新交易播放警報聲音", "trade-sounds-tooltip": "為每筆新交易播放警報聲音",
"trades": "交易", "trades": "交易",
"trigger-price": "Trigger Price", "trigger-price": "Trigger Price",
"trigger-order": "Trigger Order",
"trigger-order-rate": "Trigger {{side}} {{symbol}} price",
"trigger-orders": "Trigger Orders",
"tweet-position": "分享至Twitter", "tweet-position": "分享至Twitter",
"unrealized-pnl": "未實現盈虧", "unrealized-pnl": "未實現盈虧",
"unsettled": "未結清", "unsettled": "未結清",