mango-v4-ui/components/swap/LimitSwapForm.tsx

448 lines
14 KiB
TypeScript
Raw Normal View History

import {
useState,
useCallback,
useEffect,
useMemo,
Dispatch,
SetStateAction,
} from 'react'
import { ArrowDownIcon } from '@heroicons/react/20/solid'
import NumberFormat, {
NumberFormatValues,
SourceInfo,
} from 'react-number-format'
import Decimal from 'decimal.js'
import mangoStore from '@store/mangoStore'
import { useTranslation } from 'next-i18next'
2023-07-25 19:48:13 -07:00
import { SIZE_INPUT_UI_KEY } from '../../utils/constants'
import useLocalStorageState from 'hooks/useLocalStorageState'
import SwapSlider from './SwapSlider'
import PercentageSelectButtons from './PercentageSelectButtons'
import Select from '@components/forms/Select'
2023-07-25 19:48:13 -07:00
import { floorToDecimal } from 'utils/numbers'
import { withValueLimit } from './MarketSwapForm'
import SellTokenInput from './SellTokenInput'
import BuyTokenInput from './BuyTokenInput'
import { notify } from 'utils/notifications'
import * as sentry from '@sentry/nextjs'
import { isMangoError } from 'types'
import Button from '@components/shared/Button'
import { useWallet } from '@solana/wallet-adapter-react'
import Loading from '@components/shared/Loading'
2023-07-31 05:25:46 -07:00
import TokenLogo from '@components/shared/TokenLogo'
type LimitSwapFormProps = {
setShowTokenSelect: Dispatch<SetStateAction<'input' | 'output' | undefined>>
}
2023-07-31 05:25:46 -07:00
const ORDER_TYPES = [
// 'trade:limit',
'trade:stop-market',
'trade:stop-limit',
]
2023-07-25 19:48:13 -07:00
const set = mangoStore.getState().set
const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => {
const { t } = useTranslation(['common', 'swap', 'trade'])
const { connected } = useWallet()
const [animateSwitchArrow, setAnimateSwitchArrow] = useState(0)
const [orderType, setOrderType] = useState(ORDER_TYPES[0])
const [triggerPrice, setTriggerPrice] = useState('')
const [submitting, setSubmitting] = useState(false)
const [swapFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider')
const {
margin: useMargin,
inputBank,
outputBank,
amountIn: amountInFormValue,
amountOut: amountOutFormValue,
limitPrice,
} = mangoStore((s) => s.swap)
const amountInAsDecimal: Decimal | null = useMemo(() => {
return Number(amountInFormValue)
? new Decimal(amountInFormValue)
: new Decimal(0)
}, [amountInFormValue])
const amountOutAsDecimal: Decimal | null = useMemo(() => {
return Number(amountOutFormValue)
? new Decimal(amountOutFormValue)
: new Decimal(0)
}, [amountOutFormValue])
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) => {
set((s) => {
s.swap.amountIn = amountIn
if (!parseFloat(amountIn)) {
s.swap.amountOut = ''
}
})
}, [])
const setAmountOutFormValue = useCallback((amountOut: string) => {
set((s) => {
s.swap.amountOut = amountOut
if (!parseFloat(amountOut)) {
s.swap.amountIn = ''
}
})
}, [])
const setLimitPrice = useCallback((price: string) => {
set((s) => {
s.swap.limitPrice = price
})
}, [])
2023-07-31 05:25:46 -07:00
// set default limit and trigger price
useEffect(() => {
if (!baseBank || !quoteBank) return
2023-07-31 05:25:46 -07:00
const initialPrice = baseBank.uiPrice / quoteBank.uiPrice
if (!triggerPrice) {
setTriggerPrice((initialPrice * 0.9).toString())
}
if (!limitPrice) {
set((s) => {
2023-07-31 05:25:46 -07:00
s.swap.limitPrice = (initialPrice * 0.8).toString()
})
}
2023-07-31 05:25:46 -07:00
}, [baseBank, limitPrice, quoteBank, triggerPrice])
/*
If the use margin setting is toggled, clear the form values
*/
useEffect(() => {
setAmountInFormValue('')
setAmountOutFormValue('')
}, [useMargin, setAmountInFormValue, setAmountOutFormValue])
const handleAmountInChange = useCallback(
(e: NumberFormatValues, info: SourceInfo) => {
if (info.source !== 'event') return
setAmountInFormValue(e.value)
2023-06-17 05:22:05 -07:00
if (parseFloat(e.value) > 0 && limitPrice && outputBank) {
const amount =
outputBank.name === quoteBank?.name
? floorToDecimal(
parseFloat(e.value) * parseFloat(limitPrice),
outputBank.mintDecimals,
)
: floorToDecimal(
parseFloat(e.value) / parseFloat(limitPrice),
outputBank.mintDecimals,
)
setAmountOutFormValue(amount.toString())
}
},
[
limitPrice,
outputBank,
quoteBank,
setAmountInFormValue,
setAmountOutFormValue,
],
)
2023-06-17 05:22:05 -07:00
const handleAmountOutChange = useCallback(
(e: NumberFormatValues, info: SourceInfo) => {
if (info.source !== 'event') return
setAmountOutFormValue(e.value)
if (parseFloat(e.value) > 0 && limitPrice && inputBank) {
const amount =
outputBank?.name === quoteBank?.name
? floorToDecimal(
parseFloat(e.value) / parseFloat(limitPrice),
inputBank.mintDecimals,
)
: floorToDecimal(
parseFloat(e.value) * parseFloat(limitPrice),
inputBank.mintDecimals,
)
2023-06-17 05:22:05 -07:00
setAmountInFormValue(amount.toString())
}
},
[
inputBank,
outputBank,
limitPrice,
setAmountInFormValue,
setAmountOutFormValue,
],
2023-06-17 05:22:05 -07:00
)
const handleAmountInUi = useCallback(
(amountIn: string) => {
setAmountInFormValue(amountIn)
if (limitPrice && outputBank) {
const amount = floorToDecimal(
parseFloat(amountIn) / parseFloat(limitPrice),
2023-07-25 19:48:13 -07:00
outputBank.mintDecimals,
)
setAmountOutFormValue(amount.toString())
}
},
2023-07-25 19:48:13 -07:00
[limitPrice, outputBank, setAmountInFormValue, setAmountOutFormValue],
)
const handleLimitPrice = useCallback(
(e: NumberFormatValues, info: SourceInfo) => {
if (info.source !== 'event') return
setLimitPrice(e.value)
2023-06-17 05:22:05 -07:00
if (
parseFloat(e.value) > 0 &&
parseFloat(amountInFormValue) > 0 &&
outputBank
) {
const amount = floorToDecimal(
parseFloat(amountInFormValue) * parseFloat(e.value),
2023-07-25 19:48:13 -07:00
outputBank.mintDecimals,
2023-06-17 05:22:05 -07:00
)
setAmountOutFormValue(amount.toString())
}
},
2023-07-25 19:48:13 -07:00
[amountInFormValue, outputBank, setLimitPrice],
)
const handleTriggerPrice = useCallback(
(e: NumberFormatValues, info: SourceInfo) => {
if (info.source !== 'event') return
setTriggerPrice(e.value)
},
2023-07-25 19:48:13 -07:00
[setTriggerPrice],
)
const handleLimitSwap = useCallback(async () => {
try {
const client = mangoStore.getState().client
const group = mangoStore.getState().group
const actions = mangoStore.getState().actions
const mangoAccount = mangoStore.getState().mangoAccount.current
const inputBank = mangoStore.getState().swap.inputBank
const outputBank = mangoStore.getState().swap.outputBank
if (
!mangoAccount ||
!group ||
!inputBank ||
!outputBank ||
(!triggerPrice && orderType !== 'trade:limit') ||
(!limitPrice && orderType !== 'trade:stop-market')
)
return
setSubmitting(true)
const orderPrice =
orderType === 'trade:limit'
? parseFloat(limitPrice!)
: parseFloat(triggerPrice)
const stopLimitPrice =
orderType !== 'trade:stop-market' ? parseFloat(limitPrice!) : 0
try {
const tx = await client.tokenConditionalSwapStopLoss(
group,
mangoAccount,
inputBank.mint,
orderPrice,
outputBank.mint,
stopLimitPrice,
amountInAsDecimal.toNumber(),
null,
null,
)
notify({
title: 'Transaction confirmed',
type: 'success',
txid: tx,
noSound: true,
})
actions.fetchGroup()
await actions.reloadMangoAccount()
} catch (e) {
console.error('onSwap error: ', e)
sentry.captureException(e)
if (isMangoError(e)) {
notify({
title: 'Transaction failed',
description: e.message,
txid: e?.txid,
type: 'error',
})
}
}
} catch (e) {
console.error('Swap error:', e)
} finally {
setSubmitting(false)
}
}, [orderType, limitPrice, triggerPrice, amountInAsDecimal])
const handleSwitchTokens = useCallback(() => {
if (amountInAsDecimal?.gt(0) && limitPrice) {
const amountOut =
outputBank?.name !== quoteBank?.name
? amountInAsDecimal.mul(limitPrice)
: amountInAsDecimal.div(limitPrice)
setAmountOutFormValue(amountOut.toString())
}
set((s) => {
s.swap.inputBank = outputBank
s.swap.outputBank = inputBank
// s.swap.limitPrice = ''
})
setAnimateSwitchArrow(
2023-07-25 19:48:13 -07:00
(prevanimateSwitchArrow) => prevanimateSwitchArrow + 1,
)
}, [
setAmountInFormValue,
amountOutAsDecimal,
amountInAsDecimal,
limitPrice,
inputBank,
outputBank,
quoteBank,
])
const limitOrderDisabled =
!connected || !amountInFormValue || !amountOutFormValue
return (
<>
2023-07-25 19:48:13 -07:00
<SellTokenInput
className="rounded-b-none"
handleAmountInChange={handleAmountInChange}
setShowTokenSelect={setShowTokenSelect}
setAmountInFormValue={setAmountInFormValue}
/>
<div
className={`grid ${
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"
>
<div className="col-span-1">
<p className="mb-2 text-th-fgd-2">{t('trade:order-type')}</p>
<Select
value={t(orderType)}
onChange={(type) => setOrderType(type)}
className="w-full"
2023-07-31 05:25:46 -07:00
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">{t('trade:trigger-price')}</p>
2023-07-31 05:25:46 -07:00
<div className="relative">
<NumberFormat
inputMode="decimal"
thousandSeparator=","
allowNegative={false}
isNumericString={true}
decimalScale={outputBank?.mintDecimals || 6}
name="triggerPrice"
id="triggerPrice"
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={triggerPrice}
onValueChange={handleTriggerPrice}
isAllowed={withValueLimit}
/>
<div className="absolute top-1/2 -translate-y-1/2 left-2">
<TokenLogo bank={quoteBank} size={16} />
</div>
</div>
</div>
) : null}
{orderType !== 'trade:stop-market' ? (
<div className="col-span-1">
2023-07-31 05:25:46 -07:00
<p className="mb-2 text-th-fgd-2">{t('trade:limit-price')}</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>
</div>
) : 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)' }
}
/>
</button>
</div>
2023-07-25 19:48:13 -07:00
<BuyTokenInput
handleAmountOutChange={handleAmountOutChange}
setShowTokenSelect={setShowTokenSelect}
setAmountOutFormValue={setAmountOutFormValue}
/>
{swapFormSizeUi === 'slider' ? (
<SwapSlider
useMargin={useMargin}
amount={amountInAsDecimal.toNumber()}
2023-06-17 05:22:05 -07:00
onChange={(v) => handleAmountInUi(v)}
step={1 / 10 ** (inputBank?.mintDecimals || 6)}
/>
) : (
<PercentageSelectButtons
amountIn={amountInAsDecimal.toString()}
2023-06-17 05:22:05 -07:00
setAmountIn={(v) => handleAmountInUi(v)}
useMargin={useMargin}
/>
)}
<Button
onClick={handleLimitSwap}
className="mt-6 mb-4 flex w-full items-center justify-center text-base"
disabled={limitOrderDisabled}
size="large"
>
{submitting ? <Loading /> : t('swap:place-limit-order')}
</Button>
</>
)
}
export default LimitSwapForm