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' 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' 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' import TokenLogo from '@components/shared/TokenLogo' type LimitSwapFormProps = { setShowTokenSelect: Dispatch> } const ORDER_TYPES = [ // 'trade:limit', 'trade:stop-market', 'trade:stop-limit', ] 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 }) }, []) // set default limit and trigger price useEffect(() => { if (!baseBank || !quoteBank) return const initialPrice = baseBank.uiPrice / quoteBank.uiPrice if (!triggerPrice) { setTriggerPrice((initialPrice * 0.9).toString()) } if (!limitPrice) { set((s) => { s.swap.limitPrice = (initialPrice * 0.8).toString() }) } }, [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) 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, ], ) 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, ) setAmountInFormValue(amount.toString()) } }, [ inputBank, outputBank, limitPrice, setAmountInFormValue, setAmountOutFormValue, ], ) const handleAmountInUi = useCallback( (amountIn: string) => { setAmountInFormValue(amountIn) if (limitPrice && outputBank) { const amount = floorToDecimal( parseFloat(amountIn) / parseFloat(limitPrice), outputBank.mintDecimals, ) setAmountOutFormValue(amount.toString()) } }, [limitPrice, outputBank, setAmountInFormValue, setAmountOutFormValue], ) const handleLimitPrice = useCallback( (e: NumberFormatValues, info: SourceInfo) => { if (info.source !== 'event') return setLimitPrice(e.value) if ( parseFloat(e.value) > 0 && parseFloat(amountInFormValue) > 0 && outputBank ) { const amount = floorToDecimal( parseFloat(amountInFormValue) * parseFloat(e.value), outputBank.mintDecimals, ) setAmountOutFormValue(amount.toString()) } }, [amountInFormValue, outputBank, setLimitPrice], ) const handleTriggerPrice = useCallback( (e: NumberFormatValues, info: SourceInfo) => { if (info.source !== 'event') return setTriggerPrice(e.value) }, [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( (prevanimateSwitchArrow) => prevanimateSwitchArrow + 1, ) }, [ setAmountInFormValue, amountOutAsDecimal, amountInAsDecimal, limitPrice, inputBank, outputBank, quoteBank, ]) const limitOrderDisabled = !connected || !amountInFormValue || !amountOutFormValue return ( <>

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

{orderType !== 'trade:limit' ? (

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

) : null} {orderType !== 'trade:stop-market' ? (

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

) : null}
{swapFormSizeUi === 'slider' ? ( handleAmountInUi(v)} step={1 / 10 ** (inputBank?.mintDecimals || 6)} /> ) : ( handleAmountInUi(v)} useMargin={useMargin} /> )} ) } export default LimitSwapForm