import { useState, useCallback, useEffect, useMemo, Dispatch, SetStateAction, useLayoutEffect, } from 'react' import { ArrowDownIcon, ArrowsRightLeftIcon } 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' import { SIZE_INPUT_UI_KEY, SWAP_CHART_SETTINGS_KEY, } from '../../utils/constants' import useLocalStorageState from 'hooks/useLocalStorageState' import SwapSlider from './SwapSlider' import PercentageSelectButtons from './PercentageSelectButtons' 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, { LinkButton } from '@components/shared/Button' import Loading from '@components/shared/Loading' import TokenLogo from '@components/shared/TokenLogo' import InlineNotification from '@components/shared/InlineNotification' import { getChartPairSettings, handleFlipPrices } from './SwapTokenChart' import Select from '@components/forms/Select' import useIpAddress from 'hooks/useIpAddress' import { Bank } from '@blockworks-foundation/mango-v4' import useMangoAccount from 'hooks/useMangoAccount' type LimitSwapFormProps = { showTokenSelect: 'input' | 'output' | undefined setShowTokenSelect: Dispatch> } type LimitSwapForm = { amountIn: number hasBorrows: number | undefined triggerPrice: string } type FormErrors = Partial> type OrderTypeMultiplier = 0.9 | 1 | 1.1 enum OrderTypes { STOP_LOSS = 'trade:stop-loss', TAKE_PROFIT = 'trade:take-profit', REPAY_BORROW = 'repay-borrow', } const ORDER_TYPES = [ OrderTypes.STOP_LOSS, OrderTypes.TAKE_PROFIT, OrderTypes.REPAY_BORROW, ] const set = mangoStore.getState().set const getSellTokenBalance = (inputBank: Bank | undefined) => { const mangoAccount = mangoStore.getState().mangoAccount.current if (!inputBank || !mangoAccount) return 0 const balance = mangoAccount.getTokenBalanceUi(inputBank) return balance } const getOrderTypeMultiplier = (orderType: OrderTypes, flipPrices: boolean) => { if (orderType === OrderTypes.STOP_LOSS) { return flipPrices ? 1.1 : 0.9 } else if (orderType === OrderTypes.TAKE_PROFIT) { return flipPrices ? 0.9 : 1.1 } else { return 1 } } const LimitSwapForm = ({ showTokenSelect, setShowTokenSelect, }: LimitSwapFormProps) => { const { t } = useTranslation(['common', 'swap', 'trade']) const { mangoAccount } = useMangoAccount() const { ipAllowed, ipCountry } = useIpAddress() const [animateSwitchArrow, setAnimateSwitchArrow] = useState(0) const [triggerPrice, setTriggerPrice] = useState('') const [orderType, setOrderType] = useState(ORDER_TYPES[0]) const [orderTypeMultiplier, setOrderTypeMultiplier] = useState(null) const [submitting, setSubmitting] = useState(false) const [swapFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider') const [formErrors, setFormErrors] = useState({}) const [swapChartSettings, setSwapChartSettings] = useLocalStorageState( SWAP_CHART_SETTINGS_KEY, [], ) const { inputBank, outputBank, amountIn: amountInFormValue, amountOut: amountOutFormValue, } = mangoStore((s) => s.swap) const [inputBankName, outputBankName, inputBankDecimals, outputBankDecimals] = useMemo(() => { if (!inputBank || !outputBank) return ['', '', 0, 0] return [ inputBank.name, outputBank.name, inputBank.mintDecimals, outputBank.mintDecimals, ] }, [inputBank, outputBank]) 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 flipPrices = useMemo(() => { if (!swapChartSettings.length || !inputBankName || !outputBankName) return false const pairSettings = getChartPairSettings( swapChartSettings, inputBankName, outputBankName, ) if (pairSettings) { return pairSettings.quote === inputBankName } else return false }, [swapChartSettings, inputBankName, outputBankName]) 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 quotePrice = useMemo(() => { if (!inputBank || !outputBank) return 0 const quote = flipPrices ? floorToDecimal( outputBank.uiPrice / inputBank.uiPrice, inputBank.mintDecimals, ).toNumber() : floorToDecimal( inputBank.uiPrice / outputBank.uiPrice, outputBank.mintDecimals, ).toNumber() return quote }, [flipPrices, inputBank, outputBank]) // set default trigger price useEffect(() => { if (!quotePrice || triggerPrice || showTokenSelect) return const multiplier = getOrderTypeMultiplier(OrderTypes.STOP_LOSS, flipPrices) const decimals = !flipPrices ? inputBankDecimals : outputBankDecimals setTriggerPrice((quotePrice * multiplier).toFixed(decimals)) }, [ flipPrices, inputBankDecimals, outputBankDecimals, quotePrice, showTokenSelect, triggerPrice, ]) // flip trigger price and set amount out when chart direction is flipped useLayoutEffect(() => { if (!quotePrice) return const multiplier = getOrderTypeMultiplier(orderType, flipPrices) const decimals = flipPrices ? inputBankDecimals : outputBankDecimals const price = (quotePrice * multiplier).toFixed(decimals) setTriggerPrice(price) if (amountInAsDecimal?.gt(0)) { const amountOut = getAmountOut( amountInAsDecimal.toString(), flipPrices, price, ) setAmountOutFormValue(amountOut.toString()) } }, [flipPrices, inputBankDecimals, orderType, outputBankDecimals]) const triggerPriceDifference = useMemo(() => { if (!quotePrice) return 0 const triggerDifference = triggerPrice ? ((parseFloat(triggerPrice) - quotePrice) / quotePrice) * 100 : 0 return triggerDifference }, [quotePrice, triggerPrice]) const handleTokenSelect = (type: 'input' | 'output') => { setShowTokenSelect(type) setFormErrors({}) setTriggerPrice('') } const hasBorrowToRepay = useMemo(() => { if (orderType !== OrderTypes.REPAY_BORROW || !outputBank || !mangoAccount) return const balance = mangoAccount.getTokenBalanceUi(outputBank) const roundedBalance = floorToDecimal( balance, outputBank.mintDecimals, ).toNumber() return balance && balance < 0 ? Math.abs(roundedBalance) : 0 }, [mangoAccount, orderType, outputBank]) const isFormValid = useCallback( (form: LimitSwapForm) => { const invalidFields: FormErrors = {} setFormErrors({}) const requiredFields: (keyof LimitSwapForm)[] = [ 'amountIn', 'triggerPrice', ] const triggerPriceNumber = parseFloat(form.triggerPrice) const sellTokenBalance = getSellTokenBalance(inputBank) for (const key of requiredFields) { const value = form[key] as string if (!value) { invalidFields[key] = t('settings:error-required-field') } } if (orderType === OrderTypes.STOP_LOSS) { if (flipPrices && triggerPriceNumber <= quotePrice) { invalidFields.triggerPrice = 'Trigger price must be above oracle price' } if (!flipPrices && triggerPriceNumber >= quotePrice) { invalidFields.triggerPrice = 'Trigger price must be below oracle price' } } if (orderType === OrderTypes.TAKE_PROFIT) { if (flipPrices && triggerPriceNumber >= quotePrice) { invalidFields.triggerPrice = 'Trigger price must be below oracle price' } if (!flipPrices && triggerPriceNumber <= quotePrice) { invalidFields.triggerPrice = 'Trigger price must be above oracle price' } } if (orderType === OrderTypes.REPAY_BORROW && !hasBorrowToRepay) { invalidFields.hasBorrows = t('swap:no-borrow') } if (form.amountIn > sellTokenBalance) { invalidFields.amountIn = t('swap:insufficient-balance', { symbol: inputBank?.name, }) } if (Object.keys(invalidFields).length) { setFormErrors(invalidFields) } return invalidFields }, [ flipPrices, hasBorrowToRepay, inputBank, orderType, quotePrice, setFormErrors, ], ) // set order type multiplier on page load useEffect(() => { if (!orderTypeMultiplier) { const multiplier = getOrderTypeMultiplier(orderType, flipPrices) setOrderTypeMultiplier(multiplier) } }, [flipPrices, orderType, orderTypeMultiplier]) // get the out amount from the in amount and trigger or limit price const getAmountOut = useCallback( (amountIn: string, flipPrices: boolean, price: string) => { const amountOut = flipPrices ? floorToDecimal( parseFloat(amountIn) / parseFloat(price), outputBank?.mintDecimals || 0, ) : floorToDecimal( parseFloat(amountIn) * parseFloat(price), outputBank?.mintDecimals || 0, ) return amountOut }, [outputBank], ) // get the in amount from the out amount and trigger or limit price const getAmountIn = useCallback( (amountOut: string, flipPrices: boolean, price: string) => { const amountIn = flipPrices ? floorToDecimal( parseFloat(amountOut) * parseFloat(price), inputBank?.mintDecimals || 0, ) : floorToDecimal( parseFloat(amountOut) / parseFloat(price), inputBank?.mintDecimals || 0, ) return amountIn }, [inputBank, outputBank], ) const handleMax = useCallback( (amountIn: string) => { setAmountInFormValue(amountIn) if (parseFloat(amountIn) > 0 && triggerPrice) { const amountOut = getAmountOut(amountIn, flipPrices, triggerPrice) setAmountOutFormValue(amountOut.toString()) } }, [ flipPrices, getAmountOut, setAmountInFormValue, setAmountOutFormValue, triggerPrice, ], ) const handleRepay = useCallback( (amountOut: string) => { setAmountOutFormValue(amountOut) if (parseFloat(amountOut) > 0 && triggerPrice) { const amountIn = getAmountIn(amountOut, flipPrices, triggerPrice) setAmountInFormValue(amountIn.toString()) } }, [ flipPrices, getAmountIn, setAmountInFormValue, setAmountOutFormValue, triggerPrice, ], ) const handleAmountInChange = useCallback( (e: NumberFormatValues, info: SourceInfo) => { if (info.source !== 'event') return setFormErrors({}) setAmountInFormValue(e.value) if (parseFloat(e.value) > 0 && triggerPrice) { const amountOut = getAmountOut(e.value, flipPrices, triggerPrice) setAmountOutFormValue(amountOut.toString()) } }, [ flipPrices, getAmountOut, setAmountInFormValue, setAmountOutFormValue, setFormErrors, triggerPrice, ], ) const handleAmountOutChange = useCallback( (e: NumberFormatValues, info: SourceInfo) => { if (info.source !== 'event') return setFormErrors({}) setAmountOutFormValue(e.value) if (parseFloat(e.value) > 0 && triggerPrice) { const amountIn = getAmountIn(e.value, flipPrices, triggerPrice) setAmountInFormValue(amountIn.toString()) } }, [ flipPrices, getAmountIn, setAmountInFormValue, setAmountOutFormValue, setFormErrors, triggerPrice, ], ) const handleAmountInUi = useCallback( (amountIn: string) => { setAmountInFormValue(amountIn) setFormErrors({}) if (triggerPrice) { const amountOut = getAmountOut(amountIn, flipPrices, triggerPrice) setAmountOutFormValue(amountOut.toString()) } }, [ flipPrices, getAmountOut, setAmountInFormValue, setAmountOutFormValue, setFormErrors, triggerPrice, ], ) const handleTriggerPrice = useCallback( (e: NumberFormatValues, info: SourceInfo) => { if (info.source !== 'event') return setFormErrors({}) setTriggerPrice(e.value) if (parseFloat(e.value) > 0 && parseFloat(amountInFormValue) > 0) { const amountOut = getAmountOut(amountInFormValue, flipPrices, e.value) setAmountOutFormValue(amountOut.toString()) } }, [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(), hasBorrows: hasBorrowToRepay, triggerPrice, }) if (Object.keys(invalidFields).length) { return } 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) return setSubmitting(true) const amountIn = amountInAsDecimal.toNumber() try { let tx if (orderType === OrderTypes.REPAY_BORROW) { const amountOut = amountOutAsDecimal.toNumber() const orderPrice = parseFloat(triggerPrice) if (quotePrice > orderPrice) { tx = await client.tcsStopLossOnBorrow( group, mangoAccount, inputBank, outputBank, orderPrice, flipPrices, amountOut, null, false, null, ) } else { tx = await client.tcsTakeProfitOnBorrow( group, mangoAccount, inputBank, outputBank, orderPrice, flipPrices, amountOut, null, false, null, ) } } if (orderType === OrderTypes.STOP_LOSS) { tx = await client.tcsStopLossOnDeposit( group, mangoAccount, inputBank, outputBank, parseFloat(triggerPrice), flipPrices, amountIn, null, null, ) } if (orderType === OrderTypes.TAKE_PROFIT) { tx = await client.tcsTakeProfitOnDeposit( group, mangoAccount, inputBank, outputBank, parseFloat(triggerPrice), flipPrices, amountIn, 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) } }, [ hasBorrowToRepay, flipPrices, orderType, quotePrice, triggerPrice, amountInAsDecimal, amountOutFormValue, ]) const orderDescription = useMemo(() => { if ( !amountInFormValue || !amountOutFormValue || !inputBankName || !outputBankName || !triggerPrice ) return const quoteString = flipPrices ? `${inputBankName} per ${outputBankName}` : `${outputBankName} per ${inputBankName}` if (hasBorrowToRepay && orderType === OrderTypes.REPAY_BORROW) { const amountOut = floorToDecimal( amountOutFormValue, outputBankDecimals, ).toNumber() if (amountOut <= hasBorrowToRepay) { return t('trade:repay-borrow-order-desc', { amount: amountOut, priceUnit: quoteString, symbol: outputBankName, triggerPrice: floorToDecimal(triggerPrice, inputBankDecimals), }) } else { const depositAmount = floorToDecimal( amountOut - hasBorrowToRepay, outputBankDecimals, ).toNumber() return t('trade:repay-borrow-deposit-order-desc', { borrowAmount: hasBorrowToRepay, depositAmount: depositAmount, priceUnit: quoteString, symbol: outputBankName, triggerPrice: floorToDecimal(triggerPrice, inputBankDecimals), }) } } else { const orderTypeString = orderType === OrderTypes.STOP_LOSS ? !flipPrices ? t('trade:falls-to') : t('trade:rises-to') : !flipPrices ? t('trade:rises-to') : t('trade:falls-to') return t('trade:trigger-order-desc', { amount: floorToDecimal(amountInFormValue, inputBankDecimals), orderType: orderTypeString, priceUnit: quoteString, symbol: inputBankName, triggerPrice: floorToDecimal(triggerPrice, inputBankDecimals), }) } }, [ amountInFormValue, amountOutFormValue, flipPrices, hasBorrowToRepay, inputBankDecimals, inputBankName, orderType, outputBankDecimals, outputBankName, triggerPrice, ]) const triggerPriceSuffix = useMemo(() => { if (!inputBankName || !outputBankName) return if (flipPrices) { return `${inputBankName} per ${outputBankName}` } else { return `${outputBankName} per ${inputBankName}` } }, [flipPrices, inputBankName, outputBankName]) const toggleFlipPrices = useCallback( (flip: boolean) => { if (!inputBankName || !outputBankName) return setFormErrors({}) handleFlipPrices( flip, inputBankName, outputBankName, swapChartSettings, setSwapChartSettings, ) }, [ inputBankName, outputBankName, setFormErrors, setSwapChartSettings, swapChartSettings, ], ) const handleOrderTypeChange = useCallback( (type: string) => { setFormErrors({}) const newType = type as OrderTypes setOrderType(newType) const triggerMultiplier = getOrderTypeMultiplier(newType, flipPrices) setOrderTypeMultiplier(triggerMultiplier) const trigger = (quotePrice * triggerMultiplier).toString() setTriggerPrice(trigger) if (amountInAsDecimal.gt(0)) { const amountOut = getAmountOut( amountInAsDecimal.toString(), flipPrices, trigger, ).toString() setAmountOutFormValue(amountOut) } }, [flipPrices, quotePrice, setFormErrors, setOrderTypeMultiplier], ) // const disablePlaceOrder = // (orderType === OrderTypes.REPAY_BORROW && !hasBorrowToRepay) || // (orderType === OrderTypes.STOP_LOSS && // parseFloat(triggerPrice) > quotePrice) || // (orderType === OrderTypes.TAKE_PROFIT && // parseFloat(triggerPrice) < quotePrice) || // amountInAsDecimal.gt(sellTokenBalance) return ( <> handleTokenSelect('input')} handleMax={handleMax} isTriggerOrder />

{t('trade:order-type')}

{t('trade:trigger-price')}

= 0 ? 'text-th-up' : 'text-th-down' }`} > {triggerPriceDifference ? triggerPriceDifference.toFixed(2) : '0.00'} %

toggleFlipPrices(!flipPrices)} > {triggerPriceSuffix}
{formErrors.triggerPrice ? (
) : null}
handleTokenSelect('output')} handleRepay={ orderType === OrderTypes.REPAY_BORROW ? handleRepay : undefined } /> {swapFormSizeUi === 'slider' ? ( handleAmountInUi(v)} step={1 / 10 ** (inputBankDecimals || 6)} /> ) : ( handleAmountInUi(v)} useMargin={false} /> )} {orderType === OrderTypes.REPAY_BORROW && !hasBorrowToRepay ? null : orderDescription ? (
{orderType !== OrderTypes.REPAY_BORROW ? ( <> {t('sell')}{' '} ) : null} {orderDescription} } type="info" />
) : null} {ipAllowed ? ( ) : ( )} ) } export default LimitSwapForm