From eedc32fadb86b099dda3a2ca8ce68b94654cc961 Mon Sep 17 00:00:00 2001 From: saml33 Date: Fri, 16 Jun 2023 22:23:50 +1000 Subject: [PATCH] split swap and limit into two components --- components/BorrowForm.tsx | 2 +- components/DepositForm.tsx | 2 +- components/RepayForm.tsx | 2 +- components/WithdrawForm.tsx | 2 +- components/modals/UserSetupModal.tsx | 2 +- components/swap/LimitSwapForm.tsx | 361 +++++++++++++++++++++++ components/swap/MarketSwapForm.tsx | 325 +++++++++++++++++++++ components/swap/SwapForm.tsx | 412 +++++---------------------- components/swap/SwapSlider.tsx | 9 +- components/swap/TokenSelect.tsx | 2 +- public/locales/en/swap.json | 1 + public/locales/es/swap.json | 1 + public/locales/ru/swap.json | 1 + public/locales/zh/swap.json | 1 + public/locales/zh_tw/swap.json | 1 + store/mangoStore.ts | 2 + 16 files changed, 769 insertions(+), 357 deletions(-) create mode 100644 components/swap/LimitSwapForm.tsx create mode 100644 components/swap/MarketSwapForm.tsx diff --git a/components/BorrowForm.tsx b/components/BorrowForm.tsx index 22291de5..dc37c0e8 100644 --- a/components/BorrowForm.tsx +++ b/components/BorrowForm.tsx @@ -26,7 +26,7 @@ import Button from './shared/Button' import InlineNotification from './shared/InlineNotification' import Loading from './shared/Loading' import { EnterBottomExitBottom, FadeInFadeOut } from './shared/Transitions' -import { withValueLimit } from './swap/SwapForm' +import { withValueLimit } from './swap/MarketSwapForm' import { getMaxWithdrawForBank } from './swap/useTokenMax' import MaxAmountButton from '@components/shared/MaxAmountButton' import HealthImpactTokenChange from '@components/HealthImpactTokenChange' diff --git a/components/DepositForm.tsx b/components/DepositForm.tsx index 4fa94353..3bbf798e 100644 --- a/components/DepositForm.tsx +++ b/components/DepositForm.tsx @@ -20,7 +20,7 @@ import Label from './forms/Label' import Button from './shared/Button' import Loading from './shared/Loading' import { EnterBottomExitBottom, FadeInFadeOut } from './shared/Transitions' -import { withValueLimit } from './swap/SwapForm' +import { withValueLimit } from './swap/MarketSwapForm' import MaxAmountButton from '@components/shared/MaxAmountButton' import Tooltip from '@components/shared/Tooltip' import HealthImpactTokenChange from '@components/HealthImpactTokenChange' diff --git a/components/RepayForm.tsx b/components/RepayForm.tsx index 206af17b..a2d47309 100644 --- a/components/RepayForm.tsx +++ b/components/RepayForm.tsx @@ -16,7 +16,7 @@ import Label from './forms/Label' import Button from './shared/Button' import Loading from './shared/Loading' import { EnterBottomExitBottom, FadeInFadeOut } from './shared/Transitions' -import { withValueLimit } from './swap/SwapForm' +import { withValueLimit } from './swap/MarketSwapForm' import MaxAmountButton from '@components/shared/MaxAmountButton' import HealthImpactTokenChange from '@components/HealthImpactTokenChange' import { walletBalanceForToken } from './DepositForm' diff --git a/components/WithdrawForm.tsx b/components/WithdrawForm.tsx index 7ce387ef..421805ac 100644 --- a/components/WithdrawForm.tsx +++ b/components/WithdrawForm.tsx @@ -22,7 +22,7 @@ import Button from './shared/Button' import InlineNotification from './shared/InlineNotification' import Loading from './shared/Loading' import { EnterBottomExitBottom, FadeInFadeOut } from './shared/Transitions' -import { withValueLimit } from './swap/SwapForm' +import { withValueLimit } from './swap/MarketSwapForm' import { getMaxWithdrawForBank } from './swap/useTokenMax' import MaxAmountButton from '@components/shared/MaxAmountButton' import HealthImpactTokenChange from '@components/HealthImpactTokenChange' diff --git a/components/modals/UserSetupModal.tsx b/components/modals/UserSetupModal.tsx index 92ea4601..42d2de89 100644 --- a/components/modals/UserSetupModal.tsx +++ b/components/modals/UserSetupModal.tsx @@ -38,7 +38,7 @@ import SolBalanceWarnings from '../shared/SolBalanceWarnings' import { useEnhancedWallet } from '../wallet/EnhancedWalletProvider' import Modal from '../shared/Modal' import NumberFormat, { NumberFormatValues } from 'react-number-format' -import { withValueLimit } from '@components/swap/SwapForm' +import { withValueLimit } from '@components/swap/MarketSwapForm' import useBanksWithBalances from 'hooks/useBanksWithBalances' import BankAmountWithValue from '@components/shared/BankAmountWithValue' import { isMangoError } from 'types' diff --git a/components/swap/LimitSwapForm.tsx b/components/swap/LimitSwapForm.tsx new file mode 100644 index 00000000..da89a8fb --- /dev/null +++ b/components/swap/LimitSwapForm.tsx @@ -0,0 +1,361 @@ +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 TokenSelect from './TokenSelect' +import useDebounce from '../shared/useDebounce' +import { useTranslation } from 'next-i18next' +import { + INPUT_TOKEN_DEFAULT, + OUTPUT_TOKEN_DEFAULT, + SIZE_INPUT_UI_KEY, +} from '../../utils/constants' +import useMangoGroup from 'hooks/useMangoGroup' +import useLocalStorageState from 'hooks/useLocalStorageState' +import SwapSlider from './SwapSlider' +import MaxSwapAmount from './MaxSwapAmount' +import PercentageSelectButtons from './PercentageSelectButtons' +import useUnownedAccount from 'hooks/useUnownedAccount' +import Select from '@components/forms/Select' +import { floorToDecimal, formatCurrencyValue } from 'utils/numbers' +import { NUMBER_FORMAT_CLASSNAMES, withValueLimit } from './MarketSwapForm' + +type LimitSwapFormProps = { + setShowTokenSelect: Dispatch> +} + +const set = mangoStore.getState().set + +export const ORDER_TYPES = [ + 'trade:limit', + 'trade:stop-market', + 'trade:stop-limit', +] + +const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { + const { t } = useTranslation(['common', 'swap', 'trade']) + const [animateSwitchArrow, setAnimateSwitchArrow] = useState(0) + const [orderType, setOrderType] = useState(ORDER_TYPES[0]) + const [triggerPrice, setTriggerPrice] = useState('') + const { group } = useMangoGroup() + const [swapFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider') + const { isUnownedAccount } = useUnownedAccount() + + const { + margin: useMargin, + inputBank, + outputBank, + amountIn: amountInFormValue, + amountOut: amountOutFormValue, + limitPrice, + } = mangoStore((s) => s.swap) + const [debouncedAmountIn] = useDebounce(amountInFormValue, 300) + const [debouncedAmountOut] = useDebounce(amountOutFormValue, 300) + + const amountInAsDecimal: Decimal | null = useMemo(() => { + return Number(debouncedAmountIn) + ? new Decimal(debouncedAmountIn) + : new Decimal(0) + }, [debouncedAmountIn]) + + const amountOutAsDecimal: Decimal | null = useMemo(() => { + return Number(debouncedAmountOut) + ? new Decimal(debouncedAmountOut) + : new Decimal(0) + }, [debouncedAmountOut]) + + 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 + }) + }, []) + + /* + 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 (limitPrice && outputBank) { + const amount = floorToDecimal( + parseFloat(e.value) / parseFloat(limitPrice), + outputBank.mintDecimals + ) + setAmountOutFormValue(amount.toString()) + } + }, + [limitPrice, outputBank, setAmountInFormValue, setAmountOutFormValue] + ) + + const handleAmountIn = 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) + }, + [setLimitPrice] + ) + + const handleTriggerPrice = useCallback( + (e: NumberFormatValues, info: SourceInfo) => { + if (info.source !== 'event') return + setTriggerPrice(e.value) + }, + [setTriggerPrice] + ) + + const handleAmountOutChange = useCallback( + (e: NumberFormatValues, info: SourceInfo) => { + if (info.source !== 'event') return + setAmountOutFormValue(e.value) + if (limitPrice && inputBank) { + const amount = floorToDecimal( + parseFloat(e.value) * parseFloat(limitPrice), + inputBank.mintDecimals + ) + setAmountInFormValue(amount.toString()) + } + }, + [inputBank, limitPrice, setAmountInFormValue, setAmountOutFormValue] + ) + + const handleSwitchTokens = useCallback(() => { + if (amountInAsDecimal?.gt(0) && amountOutAsDecimal.gte(0)) { + setAmountInFormValue(amountOutAsDecimal.toString()) + } + const inputBank = mangoStore.getState().swap.inputBank + const outputBank = mangoStore.getState().swap.outputBank + set((s) => { + s.swap.inputBank = outputBank + s.swap.outputBank = inputBank + }) + setAnimateSwitchArrow( + (prevanimateSwitchArrow) => prevanimateSwitchArrow + 1 + ) + }, [setAmountInFormValue, amountOutAsDecimal, amountInAsDecimal]) + + return ( + <> +
+
+

{t('sell')}

+ {!isUnownedAccount ? ( + handleAmountIn(v)} + /> + ) : null} +
+
+ +
+
+ + + {inputBank + ? formatCurrencyValue( + inputBank.uiPrice * Number(amountInFormValue) + ) + : '–'} + +
+
+
+
+

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

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

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

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

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

+ +
+ ) : null} +
+
+ +
+
+

{t('buy')}

+
+ +
+
+ + + {formatCurrencyValue( + Number(limitPrice) * Number(amountOutFormValue) + )} + +
+
+ {swapFormSizeUi === 'slider' ? ( + handleAmountIn(v)} + step={1 / 10 ** (inputBank?.mintDecimals || 6)} + /> + ) : ( + handleAmountIn(v)} + useMargin={useMargin} + /> + )} + + ) +} + +export default LimitSwapForm diff --git a/components/swap/MarketSwapForm.tsx b/components/swap/MarketSwapForm.tsx new file mode 100644 index 00000000..47ba9e72 --- /dev/null +++ b/components/swap/MarketSwapForm.tsx @@ -0,0 +1,325 @@ +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 TokenSelect from './TokenSelect' +import useDebounce from '../shared/useDebounce' +import { useTranslation } from 'next-i18next' +import Loading from '../shared/Loading' +import { + INPUT_TOKEN_DEFAULT, + OUTPUT_TOKEN_DEFAULT, + SIZE_INPUT_UI_KEY, +} from '../../utils/constants' +import { useWallet } from '@solana/wallet-adapter-react' +import { RouteInfo } from 'types/jupiter' +import useMangoGroup from 'hooks/useMangoGroup' +import useLocalStorageState from 'hooks/useLocalStorageState' +import SwapSlider from './SwapSlider' +import MaxSwapAmount from './MaxSwapAmount' +import PercentageSelectButtons from './PercentageSelectButtons' +import useUnownedAccount from 'hooks/useUnownedAccount' +import { formatCurrencyValue } from 'utils/numbers' + +type MarketSwapFormProps = { + bestRoute: RouteInfo | undefined | null + selectedRoute: RouteInfo | undefined | null + setSelectedRoute: Dispatch> + setShowTokenSelect: Dispatch> +} + +const MAX_DIGITS = 11 +export const withValueLimit = (values: NumberFormatValues): boolean => { + return values.floatValue + ? values.floatValue.toFixed(0).length <= MAX_DIGITS + : true +} + +export const NUMBER_FORMAT_CLASSNAMES = + 'w-full rounded-r-lg h-[56px] box-border pb-4 border-l border-th-bkg-2 bg-th-input-bkg px-3 text-right font-mono text-xl text-th-fgd-1 focus:outline-none md:hover:bg-th-bkg-1' + +const set = mangoStore.getState().set + +export const ORDER_TYPES = [ + 'trade:limit', + 'trade:stop-market', + 'trade:stop-limit', +] + +const MarketSwapForm = ({ + bestRoute, + selectedRoute, + setSelectedRoute, + setShowTokenSelect, +}: MarketSwapFormProps) => { + const { t } = useTranslation(['common', 'swap', 'trade']) + const [animateSwitchArrow, setAnimateSwitchArrow] = useState(0) + const { group } = useMangoGroup() + const [swapFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider') + const { isUnownedAccount } = useUnownedAccount() + + const { + margin: useMargin, + inputBank, + outputBank, + amountIn: amountInFormValue, + amountOut: amountOutFormValue, + swapMode, + } = mangoStore((s) => s.swap) + const [debouncedAmountIn] = useDebounce(amountInFormValue, 300) + const [debouncedAmountOut] = useDebounce(amountOutFormValue, 300) + const { connected } = useWallet() + + const amountInAsDecimal: Decimal | null = useMemo(() => { + return Number(debouncedAmountIn) + ? new Decimal(debouncedAmountIn) + : new Decimal(0) + }, [debouncedAmountIn]) + + const amountOutAsDecimal: Decimal | null = useMemo(() => { + return Number(debouncedAmountOut) + ? new Decimal(debouncedAmountOut) + : new Decimal(0) + }, [debouncedAmountOut]) + + const setAmountInFormValue = useCallback( + (amountIn: string, setSwapMode?: boolean) => { + set((s) => { + s.swap.amountIn = amountIn + if (!parseFloat(amountIn)) { + s.swap.amountOut = '' + } + if (setSwapMode) { + s.swap.swapMode = 'ExactIn' + } + }) + }, + [] + ) + + const setAmountOutFormValue = useCallback((amountOut: string) => { + set((s) => { + s.swap.amountOut = amountOut + if (!parseFloat(amountOut)) { + s.swap.amountIn = '' + } + }) + }, []) + + /* + Once a route is returned from the Jupiter API, use the inAmount or outAmount + depending on the swapMode and set those values in state + */ + useEffect(() => { + if (typeof bestRoute !== 'undefined') { + setSelectedRoute(bestRoute) + + if (inputBank && swapMode === 'ExactOut' && bestRoute) { + const inAmount = new Decimal(bestRoute!.inAmount) + .div(10 ** inputBank.mintDecimals) + .toString() + setAmountInFormValue(inAmount) + } else if (outputBank && swapMode === 'ExactIn' && bestRoute) { + const outAmount = new Decimal(bestRoute!.outAmount) + .div(10 ** outputBank.mintDecimals) + .toString() + setAmountOutFormValue(outAmount) + } + } + }, [bestRoute, swapMode, inputBank, outputBank]) + + /* + 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 (swapMode === 'ExactOut') { + set((s) => { + s.swap.swapMode = 'ExactIn' + }) + } + }, + [outputBank, setAmountInFormValue, swapMode] + ) + + const handleAmountOutChange = useCallback( + (e: NumberFormatValues, info: SourceInfo) => { + if (info.source !== 'event') return + if (swapMode === 'ExactIn') { + set((s) => { + s.swap.swapMode = 'ExactOut' + }) + } + setAmountOutFormValue(e.value) + }, + [swapMode, setAmountOutFormValue] + ) + + const handleSwitchTokens = useCallback(() => { + if (amountInAsDecimal?.gt(0) && amountOutAsDecimal.gte(0)) { + setAmountInFormValue(amountOutAsDecimal.toString()) + } + const inputBank = mangoStore.getState().swap.inputBank + const outputBank = mangoStore.getState().swap.outputBank + set((s) => { + s.swap.inputBank = outputBank + s.swap.outputBank = inputBank + }) + setAnimateSwitchArrow( + (prevanimateSwitchArrow) => prevanimateSwitchArrow + 1 + ) + }, [setAmountInFormValue, amountOutAsDecimal, amountInAsDecimal]) + + const loadingSwapDetails: boolean = useMemo(() => { + return ( + !!(amountInAsDecimal.toNumber() || amountOutAsDecimal.toNumber()) && + connected && + typeof selectedRoute === 'undefined' + ) + }, [amountInAsDecimal, amountOutAsDecimal, connected, selectedRoute]) + + return ( + <> +
+
+

{t('sell')}

+ {!isUnownedAccount ? ( + setAmountInFormValue(v, true)} + /> + ) : null} +
+
+ +
+
+ + + {inputBank + ? formatCurrencyValue( + inputBank.uiPrice * Number(amountInFormValue) + ) + : '–'} + +
+
+
+ +
+
+

{t('buy')}

+
+ +
+
+ {loadingSwapDetails ? ( +
+ +
+ ) : ( + <> + + + {outputBank + ? formatCurrencyValue( + outputBank.uiPrice * Number(amountOutFormValue) + ) + : '–'} + + + )} +
+
+ {swapFormSizeUi === 'slider' ? ( + setAmountInFormValue(v, true)} + step={1 / 10 ** (inputBank?.mintDecimals || 6)} + /> + ) : ( + setAmountInFormValue(v, true)} + useMargin={useMargin} + /> + )} + + ) +} + +export default MarketSwapForm diff --git a/components/swap/SwapForm.tsx b/components/swap/SwapForm.tsx index 0ca5f1a1..123e2c8c 100644 --- a/components/swap/SwapForm.tsx +++ b/components/swap/SwapForm.tsx @@ -1,20 +1,14 @@ -import { useState, useCallback, useEffect, useMemo } from 'react' +import { useState, useCallback, useMemo } from 'react' import { PublicKey } from '@solana/web3.js' import { - ArrowDownIcon, Cog8ToothIcon, ExclamationCircleIcon, LinkIcon, } from '@heroicons/react/20/solid' -import NumberFormat, { - NumberFormatValues, - SourceInfo, -} from 'react-number-format' import Decimal from 'decimal.js' import mangoStore from '@store/mangoStore' import ContentBox from '../shared/ContentBox' import SwapReviewRouteInfo from './SwapReviewRouteInfo' -import TokenSelect from './TokenSelect' import useDebounce from '../shared/useDebounce' import { useTranslation } from 'next-i18next' import SwapFormTokenList from './SwapFormTokenList' @@ -24,24 +18,14 @@ import Loading from '../shared/Loading' import { EnterBottomExitBottom } from '../shared/Transitions' import useQuoteRoutes from './useQuoteRoutes' import { HealthType } from '@blockworks-foundation/mango-v4' -import { - INPUT_TOKEN_DEFAULT, - MANGO_MINT, - OUTPUT_TOKEN_DEFAULT, - SIZE_INPUT_UI_KEY, - USDC_MINT, -} from '../../utils/constants' +import { MANGO_MINT, USDC_MINT } from '../../utils/constants' import { useTokenMax } from './useTokenMax' import HealthImpact from '@components/shared/HealthImpact' import { useWallet } from '@solana/wallet-adapter-react' import useMangoAccount from 'hooks/useMangoAccount' import { RouteInfo } from 'types/jupiter' import useMangoGroup from 'hooks/useMangoGroup' -import useLocalStorageState from 'hooks/useLocalStorageState' -import SwapSlider from './SwapSlider' import TokenVaultWarnings from '@components/shared/TokenVaultWarnings' -import MaxSwapAmount from './MaxSwapAmount' -import PercentageSelectButtons from './PercentageSelectButtons' import useIpAddress from 'hooks/useIpAddress' import { useEnhancedWallet } from '@components/wallet/EnhancedWalletProvider' import SwapSettings from './SwapSettings' @@ -49,18 +33,8 @@ import InlineNotification from '@components/shared/InlineNotification' import useUnownedAccount from 'hooks/useUnownedAccount' import Tooltip from '@components/shared/Tooltip' import TabUnderline from '@components/shared/TabUnderline' -import Select from '@components/forms/Select' -import { formatCurrencyValue } from 'utils/numbers' - -const MAX_DIGITS = 11 -export const withValueLimit = (values: NumberFormatValues): boolean => { - return values.floatValue - ? values.floatValue.toFixed(0).length <= MAX_DIGITS - : true -} - -const NUMBER_FORMAT_CLASSNAMES = - 'w-full rounded-r-lg h-[54px] box-border pb-3 border-l border-th-bkg-2 bg-th-input-bkg px-3 text-right font-mono text-xl text-th-fgd-1 focus:outline-none md:hover:border-th-input-border-hover focus-visible:bg-th-bkg-3' +import MarketSwapForm from './MarketSwapForm' +import LimitSwapForm from './LimitSwapForm' const set = mangoStore.getState().set @@ -74,18 +48,12 @@ const SwapForm = () => { const { t } = useTranslation(['common', 'swap', 'trade']) //initial state is undefined null is returned on error const [selectedRoute, setSelectedRoute] = useState() - const [animateSwitchArrow, setAnimateSwitchArrow] = useState(0) const [showTokenSelect, setShowTokenSelect] = useState<'input' | 'output'>() const [showSettings, setShowSettings] = useState(false) const [showConfirm, setShowConfirm] = useState(false) - const [orderType, setOrderType] = useState(ORDER_TYPES[0]) - const [activeTab, setActiveTab] = useState('swap') - const [limitPrice, setLimitPrice] = useState('') - const [triggerPrice, setTriggerPrice] = useState('') + const [swapOrLimit, setSwapOrLimit] = useState('swap') const { group } = useMangoGroup() - const [swapFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider') const { ipAllowed, ipCountry } = useIpAddress() - const { isUnownedAccount } = useUnownedAccount() const { margin: useMargin, @@ -123,102 +91,6 @@ const SwapForm = () => { wallet: publicKey?.toBase58(), }) - const setAmountInFormValue = useCallback( - (amountIn: string, setSwapMode?: boolean) => { - set((s) => { - s.swap.amountIn = amountIn - if (!parseFloat(amountIn)) { - s.swap.amountOut = '' - } - if (setSwapMode) { - s.swap.swapMode = 'ExactIn' - } - }) - }, - [] - ) - - const setAmountOutFormValue = useCallback((amountOut: string) => { - set((s) => { - s.swap.amountOut = amountOut - if (!parseFloat(amountOut)) { - s.swap.amountIn = '' - } - }) - }, []) - - /* - Once a route is returned from the Jupiter API, use the inAmount or outAmount - depending on the swapMode and set those values in state - */ - useEffect(() => { - if (typeof bestRoute !== 'undefined') { - setSelectedRoute(bestRoute) - - if (inputBank && swapMode === 'ExactOut' && bestRoute) { - const inAmount = new Decimal(bestRoute!.inAmount) - .div(10 ** inputBank.mintDecimals) - .toString() - setAmountInFormValue(inAmount) - } else if (outputBank && swapMode === 'ExactIn' && bestRoute) { - const outAmount = new Decimal(bestRoute!.outAmount) - .div(10 ** outputBank.mintDecimals) - .toString() - setAmountOutFormValue(outAmount) - } - } - }, [bestRoute, swapMode, inputBank, outputBank]) - - /* - 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 - if (swapMode === 'ExactOut') { - set((s) => { - s.swap.swapMode = 'ExactIn' - }) - } - setAmountInFormValue(e.value) - }, - [swapMode, setAmountInFormValue] - ) - - const handleLimitPrice = useCallback( - (e: NumberFormatValues, info: SourceInfo) => { - if (info.source !== 'event') return - setLimitPrice(e.value) - }, - [setLimitPrice] - ) - - const handleTriggerPrice = useCallback( - (e: NumberFormatValues, info: SourceInfo) => { - if (info.source !== 'event') return - setTriggerPrice(e.value) - }, - [setTriggerPrice] - ) - - const handleAmountOutChange = useCallback( - (e: NumberFormatValues, info: SourceInfo) => { - if (info.source !== 'event') return - if (swapMode === 'ExactIn') { - set((s) => { - s.swap.swapMode = 'ExactOut' - }) - } - setAmountOutFormValue(e.value) - }, - [swapMode, setAmountOutFormValue] - ) - const handleTokenInSelect = useCallback((mintAddress: string) => { const group = mangoStore.getState().group if (group) { @@ -241,21 +113,6 @@ const SwapForm = () => { setShowTokenSelect(undefined) }, []) - const handleSwitchTokens = useCallback(() => { - if (amountInAsDecimal?.gt(0) && amountOutAsDecimal.gte(0)) { - setAmountInFormValue(amountOutAsDecimal.toString()) - } - const inputBank = mangoStore.getState().swap.inputBank - const outputBank = mangoStore.getState().swap.outputBank - set((s) => { - s.swap.inputBank = outputBank - s.swap.outputBank = inputBank - }) - setAnimateSwitchArrow( - (prevanimateSwitchArrow) => prevanimateSwitchArrow + 1 - ) - }, [setAmountInFormValue, amountOutAsDecimal, amountInAsDecimal]) - const maintProjectedHealth = useMemo(() => { const group = mangoStore.getState().group if ( @@ -298,10 +155,36 @@ const SwapForm = () => { const loadingSwapDetails: boolean = useMemo(() => { return ( !!(amountInAsDecimal.toNumber() || amountOutAsDecimal.toNumber()) && + swapOrLimit === 'swap' && connected && typeof selectedRoute === 'undefined' ) - }, [amountInAsDecimal, amountOutAsDecimal, connected, selectedRoute]) + }, [ + amountInAsDecimal, + amountOutAsDecimal, + connected, + selectedRoute, + swapOrLimit, + ]) + + const handleSwapOrLimit = useCallback( + (orderType: string) => { + setSwapOrLimit(orderType) + if (orderType === 'trade:limit' && outputBank) { + set((s) => { + s.swap.limitPrice = outputBank.uiPrice.toString() + }) + } + }, + [outputBank, set, setSwapOrLimit] + ) + + const handlePlaceOrder = () => { + console.log('place swap limit order') + } + + const limitOrderDisabled = + !connected || !amountInFormValue || !amountOutFormValue return ( {
setActiveTab(v)} + onChange={(v) => handleSwapOrLimit(v)} />
@@ -366,207 +249,40 @@ const SwapForm = () => {
-
-
-

{t('sell')}

- {!isUnownedAccount ? ( - setAmountInFormValue(v, true)} - /> - ) : null} -
-
- -
-
- - - {inputBank - ? formatCurrencyValue( - inputBank.uiPrice * Number(amountInFormValue) - ) - : '–'} - -
-
- {activeTab === 'trade:limit' ? ( -
-
-

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

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

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

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

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

- -
- ) : null} -
- ) : null} -
- -
-
-

{t('buy')}

-
- -
-
- {loadingSwapDetails ? ( -
- -
- ) : ( - <> - - - {outputBank - ? formatCurrencyValue( - outputBank.uiPrice * Number(amountOutFormValue) - ) - : '–'} - - - )} -
-
- {swapFormSizeUi === 'slider' ? ( - setAmountInFormValue(v, true)} - step={1 / 10 ** (inputBank?.mintDecimals || 6)} + {swapOrLimit === 'swap' ? ( + ) : ( - setAmountInFormValue(v, true)} - useMargin={useMargin} - /> + )} {ipAllowed ? ( - + swapOrLimit === 'swap' ? ( + + ) : ( + + ) ) : (