From b548e7f61e9506c496b5f84fa4f420fadb2546c5 Mon Sep 17 00:00:00 2001 From: saml33 Date: Thu, 15 Jun 2023 14:42:34 +1000 Subject: [PATCH 01/44] ui for swap limit orders --- components/forms/Select.tsx | 4 +- components/swap/SwapForm.tsx | 127 ++++++++++++++++++++++++----- components/swap/SwapTokenChart.tsx | 2 +- components/swap/TokenSelect.tsx | 2 +- public/locales/en/trade.json | 3 + public/locales/es/trade.json | 3 + public/locales/ru/trade.json | 3 + public/locales/zh/trade.json | 3 + public/locales/zh_tw/trade.json | 3 + 9 files changed, 128 insertions(+), 22 deletions(-) diff --git a/components/forms/Select.tsx b/components/forms/Select.tsx index 1a7183c7..7884b656 100644 --- a/components/forms/Select.tsx +++ b/components/forms/Select.tsx @@ -7,6 +7,7 @@ interface SelectProps { onChange: (x: string) => void children: ReactNode className?: string + buttonClassName?: string dropdownPanelClassName?: string placeholder?: string disabled?: boolean @@ -17,6 +18,7 @@ const Select = ({ onChange, children, className, + buttonClassName, dropdownPanelClassName, placeholder = 'Select', disabled = false, @@ -27,7 +29,7 @@ const Select = ({ {({ open }) => ( <>
{ @@ -57,10 +59,16 @@ export const withValueLimit = (values: NumberFormatValues): boolean => { } const NUMBER_FORMAT_CLASSNAMES = - 'w-full rounded-lg rounded-l-none border border-th-input-border bg-th-input-bkg p-3 text-right font-mono text-xl text-th-fgd-1 focus:border-th-fgd-4 focus:outline-none md:hover:border-th-input-border-hover md:hover:focus-visible:border-th-fgd-4' + 'w-full rounded-lg rounded-l-none h-[54px] border-l border-th-bkg-2 bg-th-input-bkg p-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' const set = mangoStore.getState().set +export const ORDER_TYPES = [ + 'trade:limit', + 'trade:stop-market', + 'trade:stop-limit', +] + const SwapForm = () => { const { t } = useTranslation(['common', 'swap', 'trade']) //initial state is undefined null is returned on error @@ -69,6 +77,8 @@ const SwapForm = () => { 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 { group } = useMangoGroup() const [swapFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider') const { ipAllowed, ipCountry } = useIpAddress() @@ -277,7 +287,7 @@ const SwapForm = () => { return (
{ > setShowSettings(false)} /> -
+
+
+ setActiveTab(v)} + /> +
{
-
-

{t('swap:pay')}

- {!isUnownedAccount ? ( - setAmountInFormValue(v, true)} - /> - ) : null} -
-
+
+
+

{t('sell')}

+ {!isUnownedAccount ? ( + setAmountInFormValue(v, true)} + /> + ) : null} +
{ type="input" />
-
+
{ />
-
+ {activeTab === 'trade:limit' ? ( +
+
+

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

+ +
+
+

+ {orderType === 'trade:limit' + ? t('trade:limit-price') + : t('trade:trigger-price')} +

+ +
+ {orderType === 'trade:stop-limit' ? ( +
+

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

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

{t('swap:receive')}

-
+
+

{t('buy')}

{ type="output" />
-
+
{loadingSwapDetails ? (
diff --git a/components/swap/SwapTokenChart.tsx b/components/swap/SwapTokenChart.tsx index 7611ae95..50902a0f 100644 --- a/components/swap/SwapTokenChart.tsx +++ b/components/swap/SwapTokenChart.tsx @@ -390,7 +390,7 @@ const SwapTokenChart = () => { )}
-
+
{ return ( +
+
+

{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' ? ( + + ) : ( + + ) ) : (
@@ -307,7 +318,7 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => {

{t('buy')}

@@ -334,9 +345,11 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { onValueChange={handleAmountOutChange} /> - {formatCurrencyValue( - Number(limitPrice) * Number(amountOutFormValue) - )} + {parseFloat(limitPrice!) && parseFloat(amountOutFormValue) + ? formatCurrencyValue( + Number(limitPrice) * Number(amountOutFormValue) + ) + : '$0.00'}
@@ -344,13 +357,13 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { handleAmountIn(v)} + onChange={(v) => handleAmountInUi(v)} step={1 / 10 ** (inputBank?.mintDecimals || 6)} /> ) : ( handleAmountIn(v)} + setAmountIn={(v) => handleAmountInUi(v)} useMargin={useMargin} /> )} diff --git a/components/swap/MarketSwapForm.tsx b/components/swap/MarketSwapForm.tsx index 47ba9e72..452ede95 100644 --- a/components/swap/MarketSwapForm.tsx +++ b/components/swap/MarketSwapForm.tsx @@ -261,7 +261,7 @@ const MarketSwapForm = ({

{t('buy')}

diff --git a/components/swap/PercentageSelectButtons.tsx b/components/swap/PercentageSelectButtons.tsx index 35e26c3f..2891d237 100644 --- a/components/swap/PercentageSelectButtons.tsx +++ b/components/swap/PercentageSelectButtons.tsx @@ -45,14 +45,12 @@ const PercentageSelectButtons = ({ } return ( -
- handleSizePercentage(p)} - values={['10', '25', '50', '75', '100']} - unit="%" - /> -
+ handleSizePercentage(p)} + values={['10', '25', '50', '75', '100']} + unit="%" + /> ) } From 7e600cfd203769126fd71a69ad15a412d14e3889 Mon Sep 17 00:00:00 2001 From: saml33 Date: Mon, 31 Jul 2023 16:05:26 +1000 Subject: [PATCH 06/44] test placing orders and swap orders table --- components/swap/LimitSwapForm.tsx | 198 ++++++++++++++++++--- components/swap/SwapForm.tsx | 25 +-- components/swap/SwapInfoTabs.tsx | 3 + components/swap/SwapOrders.tsx | 276 ++++++++++++++++++++++++++++++ 4 files changed, 453 insertions(+), 49 deletions(-) create mode 100644 components/swap/SwapOrders.tsx diff --git a/components/swap/LimitSwapForm.tsx b/components/swap/LimitSwapForm.tsx index e9e87fe4..5f7bb502 100644 --- a/components/swap/LimitSwapForm.tsx +++ b/components/swap/LimitSwapForm.tsx @@ -13,7 +13,6 @@ import NumberFormat, { } from 'react-number-format' import Decimal from 'decimal.js' import mangoStore from '@store/mangoStore' -import useDebounce from '../shared/useDebounce' import { useTranslation } from 'next-i18next' import { SIZE_INPUT_UI_KEY } from '../../utils/constants' import useLocalStorageState from 'hooks/useLocalStorageState' @@ -24,6 +23,12 @@ 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' type LimitSwapFormProps = { setShowTokenSelect: Dispatch> @@ -35,9 +40,11 @@ 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 { @@ -48,20 +55,28 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { 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) + return Number(amountInFormValue) + ? new Decimal(amountInFormValue) : new Decimal(0) - }, [debouncedAmountIn]) + }, [amountInFormValue]) const amountOutAsDecimal: Decimal | null = useMemo(() => { - return Number(debouncedAmountOut) - ? new Decimal(debouncedAmountOut) + return Number(amountOutFormValue) + ? new Decimal(amountOutFormValue) : new Decimal(0) - }, [debouncedAmountOut]) + }, [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) => { @@ -87,6 +102,16 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { }) }, []) + useEffect(() => { + if (!baseBank || !quoteBank) return + const initialLimitPrice = baseBank.uiPrice / quoteBank.uiPrice + if (!limitPrice) { + set((s) => { + s.swap.limitPrice = initialLimitPrice.toString() + }) + } + }, [baseBank, limitPrice, quoteBank]) + /* If the use margin setting is toggled, clear the form values */ @@ -100,14 +125,26 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { if (info.source !== 'event') return setAmountInFormValue(e.value) if (parseFloat(e.value) > 0 && limitPrice && outputBank) { - const amount = floorToDecimal( - parseFloat(e.value) / parseFloat(limitPrice), - outputBank.mintDecimals, - ) + 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, setAmountInFormValue, setAmountOutFormValue], + [ + limitPrice, + outputBank, + quoteBank, + setAmountInFormValue, + setAmountOutFormValue, + ], ) const handleAmountOutChange = useCallback( @@ -115,14 +152,26 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { if (info.source !== 'event') return setAmountOutFormValue(e.value) if (parseFloat(e.value) > 0 && limitPrice && inputBank) { - const amount = floorToDecimal( - parseFloat(e.value) * parseFloat(limitPrice), - inputBank.mintDecimals, - ) + 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, limitPrice, setAmountInFormValue, setAmountOutFormValue], + [ + inputBank, + outputBank, + limitPrice, + setAmountInFormValue, + setAmountOutFormValue, + ], ) const handleAmountInUi = useCallback( @@ -149,7 +198,7 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { outputBank ) { const amount = floorToDecimal( - parseFloat(amountInFormValue) / parseFloat(e.value), + parseFloat(amountInFormValue) * parseFloat(e.value), outputBank.mintDecimals, ) setAmountOutFormValue(amount.toString()) @@ -166,20 +215,101 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { [setTriggerPrice], ) - const handleSwitchTokens = useCallback(() => { - if (amountInAsDecimal?.gt(0) && amountOutAsDecimal.gte(0)) { - setAmountInFormValue(amountOutAsDecimal.toString()) + 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()) } - const inputBank = mangoStore.getState().swap.inputBank - const outputBank = mangoStore.getState().swap.outputBank set((s) => { s.swap.inputBank = outputBank s.swap.outputBank = inputBank + // s.swap.limitPrice = '' }) setAnimateSwitchArrow( (prevanimateSwitchArrow) => prevanimateSwitchArrow + 1, ) - }, [setAmountInFormValue, amountOutAsDecimal, amountInAsDecimal]) + }, [ + setAmountInFormValue, + amountOutAsDecimal, + amountInAsDecimal, + limitPrice, + inputBank, + outputBank, + quoteBank, + ]) + + const limitOrderDisabled = + !connected || !amountInFormValue || !amountOutFormValue return ( <> @@ -231,7 +361,13 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { ) : null} {orderType !== 'trade:stop-market' ? (
-

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

+

+ {t('trade:limit-price')} + + {' '} + ({quoteBank?.name}) + +

{ useMargin={useMargin} /> )} + ) } diff --git a/components/swap/SwapForm.tsx b/components/swap/SwapForm.tsx index cc39fda6..9fc1994d 100644 --- a/components/swap/SwapForm.tsx +++ b/components/swap/SwapForm.tsx @@ -94,6 +94,7 @@ const SwapForm = () => { const bank = group.getFirstBankByMint(new PublicKey(mintAddress)) set((s) => { s.swap.inputBank = bank + s.swap.limitPrice = '' }) } setShowTokenSelect(undefined) @@ -105,6 +106,7 @@ const SwapForm = () => { const bank = group.getFirstBankByMint(new PublicKey(mintAddress)) set((s) => { s.swap.outputBank = bank + s.swap.limitPrice = '' }) } setShowTokenSelect(undefined) @@ -167,19 +169,10 @@ const SwapForm = () => { 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 handleSetMargin = () => { set((s) => { s.swap.margin = !s.swap.margin @@ -190,9 +183,6 @@ const SwapForm = () => { setSavedSwapMargin(useMargin) }, [useMargin]) - const limitOrderDisabled = - !connected || !amountInFormValue || !amountOutFormValue - return ( { selectedRoute ? amountOutAsDecimal.toNumber() : undefined } /> - ) : ( - - ) + ) : null ) : (
{selectedTab === 'balances' ? : null} + {selectedTab === 'orders' ? : null} {selectedTab === 'swap:swap-history' ? : null}
) diff --git a/components/swap/SwapOrders.tsx b/components/swap/SwapOrders.tsx new file mode 100644 index 00000000..4ddaa9fa --- /dev/null +++ b/components/swap/SwapOrders.tsx @@ -0,0 +1,276 @@ +import { IconButton } from '@components/shared/Button' +import ConnectEmptyState from '@components/shared/ConnectEmptyState' +import { + SortableColumnHeader, + Table, + Td, + Th, + TrBody, + TrHead, +} from '@components/shared/TableElements' +import { NoSymbolIcon, TrashIcon } from '@heroicons/react/20/solid' +import { BN } from '@project-serum/anchor' +import { useWallet } from '@solana/wallet-adapter-react' +import mangoStore from '@store/mangoStore' +import useMangoAccount from 'hooks/useMangoAccount' +import useMangoGroup from 'hooks/useMangoGroup' +import { useSortableData } from 'hooks/useSortableData' +// import { useViewport } from 'hooks/useViewport' +import { useCallback, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { notify } from 'utils/notifications' +import { floorToDecimal } from 'utils/numbers' +// import { breakpoints } from 'utils/theme' +import * as sentry from '@sentry/nextjs' +import { isMangoError } from 'types' +import Loading from '@components/shared/Loading' + +const SwapOrders = () => { + const { t } = useTranslation(['common', 'swap', 'trade']) + // const { width } = useViewport() + // const showTableView = width ? width > breakpoints.md : false + const { mangoAccount, mangoAccountAddress } = useMangoAccount() + const { group } = useMangoGroup() + const { connected } = useWallet() + const [cancelId, setCancelId] = useState('') + + const orders = useMemo(() => { + if (!mangoAccount) return [] + return mangoAccount.tokenConditionalSwaps.filter((tcs) => tcs.hasData) + }, [mangoAccount]) + + const formattedTableData = useCallback(() => { + if (!group) return [] + const formatted = [] + for (const order of orders) { + const buyBank = group.getFirstBankByTokenIndex(order.buyTokenIndex) + const sellBank = group.getFirstBankByTokenIndex(order.sellTokenIndex) + const market = `${sellBank.name}/${buyBank.name}` + const size = floorToDecimal( + order.getMaxSellUi(group), + sellBank.mintDecimals, + ).toNumber() + const triggerPrice = order.getPriceLowerLimitUi(group) + const limitPrice = order.getPriceUpperLimitUi(group) + const pricePremium = order.getPricePremium() + + const orderType = + limitPrice === 0 + ? 'trade:stop-market' + : triggerPrice === limitPrice + ? 'trade:limit' + : 'trade:stop-limit' + + const data = { + ...order, + buyBank, + sellBank, + market, + size, + triggerPrice, + limitPrice, + orderType, + fee: pricePremium, + } + formatted.push(data) + } + return formatted + }, [group, orders]) + + const { + items: tableData, + requestSort, + sortConfig, + } = useSortableData(formattedTableData()) + + const handleCancel = async (id: BN) => { + try { + const client = mangoStore.getState().client + const group = mangoStore.getState().group + const actions = mangoStore.getState().actions + const mangoAccount = mangoStore.getState().mangoAccount.current + + if (!mangoAccount || !group) return + setCancelId(id.toString()) + + try { + const tx = await client.tokenConditionalSwapCancel( + group, + mangoAccount, + id, + ) + notify({ + title: 'Transaction confirmed', + type: 'success', + txid: tx, + noSound: true, + }) + actions.fetchGroup() + await actions.reloadMangoAccount() + } catch (e) { + console.error('failed to cancel swap order', e) + sentry.captureException(e) + if (isMangoError(e)) { + notify({ + title: 'Transaction failed', + description: e.message, + txid: e?.txid, + type: 'error', + }) + } + } + } catch (e) { + console.error('failed to cancel swap order', e) + } finally { + setCancelId('') + } + } + + return orders.length ? ( + + + + + + + + + + + + + + {tableData.map((data, i) => { + const { + buyBank, + fee, + market, + orderType, + limitPrice, + sellBank, + size, + triggerPrice, + } = data + return ( + + + + + + + + + + ) + })} + +
+ requestSort('market')} + sortConfig={sortConfig} + title={t('market')} + /> + +
+ requestSort('orderType')} + sortConfig={sortConfig} + title={t('order-type')} + /> +
+
+
+ requestSort('size')} + sortConfig={sortConfig} + title={t('size')} + /> +
+
+
+ requestSort('triggerPrice')} + sortConfig={sortConfig} + title={t('trigger-price')} + /> +
+
+
+ requestSort('limitPrice')} + sortConfig={sortConfig} + title={t('limit-price')} + /> +
+
+
+ requestSort('fee')} + sortConfig={sortConfig} + title={t('fee')} + /> +
+
{t('cancel')}
{market} +

{t(orderType)}

+
+

+ {size} + + {' '} + {sellBank.name} + +

+
+ {triggerPrice !== limitPrice ? ( +

+ {triggerPrice} + + {' '} + {buyBank.name} + +

+ ) : ( +

+ )} +
+ {limitPrice ? ( +

+ {limitPrice} + + {' '} + {buyBank.name} + +

+ ) : ( +

+ )} +
+

{fee.toFixed(2)}%

+
+ handleCancel(data.id)} size="small"> + {cancelId === data.id.toString() ? ( + + ) : ( + + )} + +
+ ) : mangoAccountAddress || connected ? ( +
+ +

{t('trade:no-orders')}

+
+ ) : ( +
+ +
+ ) +} + +export default SwapOrders From 2e0076bd737c5b7d9edb68aac6ac84ef9db219b5 Mon Sep 17 00:00:00 2001 From: saml33 Date: Mon, 31 Jul 2023 22:25:46 +1000 Subject: [PATCH 07/44] fixes --- components/swap/LimitSwapForm.tsx | 93 ++++++++++++++++++------------- components/swap/SwapInfoTabs.tsx | 4 +- components/swap/SwapOrders.tsx | 52 +++++++++++------ components/swap/SwapPage.tsx | 4 +- public/locales/en/swap.json | 1 + public/locales/en/trade.json | 1 + public/locales/es/swap.json | 1 + public/locales/es/trade.json | 1 + public/locales/ru/swap.json | 1 + public/locales/ru/trade.json | 1 + public/locales/zh/swap.json | 1 + public/locales/zh/trade.json | 1 + public/locales/zh_tw/swap.json | 1 + public/locales/zh_tw/trade.json | 1 + 14 files changed, 102 insertions(+), 61 deletions(-) diff --git a/components/swap/LimitSwapForm.tsx b/components/swap/LimitSwapForm.tsx index 5f7bb502..9d67e0cc 100644 --- a/components/swap/LimitSwapForm.tsx +++ b/components/swap/LimitSwapForm.tsx @@ -29,12 +29,17 @@ 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 ORDER_TYPES = [ + // 'trade:limit', + 'trade:stop-market', + 'trade:stop-limit', +] const set = mangoStore.getState().set @@ -102,15 +107,19 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { }) }, []) + // set default limit and trigger price useEffect(() => { if (!baseBank || !quoteBank) return - const initialLimitPrice = baseBank.uiPrice / quoteBank.uiPrice + const initialPrice = baseBank.uiPrice / quoteBank.uiPrice + if (!triggerPrice) { + setTriggerPrice((initialPrice * 0.9).toString()) + } if (!limitPrice) { set((s) => { - s.swap.limitPrice = initialLimitPrice.toString() + s.swap.limitPrice = (initialPrice * 0.8).toString() }) } - }, [baseBank, limitPrice, quoteBank]) + }, [baseBank, limitPrice, quoteBank, triggerPrice]) /* If the use margin setting is toggled, clear the form values @@ -331,7 +340,7 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { 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" + 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) => ( @@ -343,45 +352,49 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { {orderType !== 'trade:limit' ? (

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

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

- {t('trade:limit-price')} - - {' '} - ({quoteBank?.name}) - -

- +

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

+
+ +
+ +
+
) : null}
diff --git a/components/swap/SwapInfoTabs.tsx b/components/swap/SwapInfoTabs.tsx index e076462f..3408b747 100644 --- a/components/swap/SwapInfoTabs.tsx +++ b/components/swap/SwapInfoTabs.tsx @@ -19,7 +19,7 @@ const SwapInfoTabs = () => { const tabsWithCount: [string, number][] = useMemo(() => { return [ ['balances', 0], - ['orders', 0], + ['trade:orders', 0], ['swap:swap-history', 0], ] }, [openOrders, mangoAccount]) @@ -40,7 +40,7 @@ const SwapInfoTabs = () => { />
{selectedTab === 'balances' ? : null} - {selectedTab === 'orders' ? : null} + {selectedTab === 'trade:orders' ? : null} {selectedTab === 'swap:swap-history' ? : null}
) diff --git a/components/swap/SwapOrders.tsx b/components/swap/SwapOrders.tsx index 4ddaa9fa..3b0e80d7 100644 --- a/components/swap/SwapOrders.tsx +++ b/components/swap/SwapOrders.tsx @@ -45,7 +45,7 @@ const SwapOrders = () => { for (const order of orders) { const buyBank = group.getFirstBankByTokenIndex(order.buyTokenIndex) const sellBank = group.getFirstBankByTokenIndex(order.sellTokenIndex) - const market = `${sellBank.name}/${buyBank.name}` + const pair = `${sellBank.name}/${buyBank.name}` const size = floorToDecimal( order.getMaxSellUi(group), sellBank.mintDecimals, @@ -53,20 +53,18 @@ const SwapOrders = () => { const triggerPrice = order.getPriceLowerLimitUi(group) const limitPrice = order.getPriceUpperLimitUi(group) const pricePremium = order.getPricePremium() + const filled = order.getSoldUi(group) const orderType = - limitPrice === 0 - ? 'trade:stop-market' - : triggerPrice === limitPrice - ? 'trade:limit' - : 'trade:stop-limit' + limitPrice === 0 ? 'trade:stop-market' : 'trade:stop-limit' const data = { ...order, buyBank, sellBank, - market, + pair, size, + filled, triggerPrice, limitPrice, orderType, @@ -132,10 +130,10 @@ const SwapOrders = () => { requestSort('market')} + sortKey="pair" + sort={() => requestSort('pair')} sortConfig={sortConfig} - title={t('market')} + title={t('swap:pair')} /> @@ -144,7 +142,7 @@ const SwapOrders = () => { sortKey="orderType" sort={() => requestSort('orderType')} sortConfig={sortConfig} - title={t('order-type')} + title={t('trade:order-type')} />
@@ -154,7 +152,17 @@ const SwapOrders = () => { sortKey="size" sort={() => requestSort('size')} sortConfig={sortConfig} - title={t('size')} + title={t('trade:size')} + /> +
+ + +
+ requestSort('filled')} + sortConfig={sortConfig} + title={t('trade:filled')} />
@@ -164,7 +172,7 @@ const SwapOrders = () => { sortKey="triggerPrice" sort={() => requestSort('triggerPrice')} sortConfig={sortConfig} - title={t('trigger-price')} + title={t('trade:trigger-price')} />
@@ -174,7 +182,7 @@ const SwapOrders = () => { sortKey="limitPrice" sort={() => requestSort('limitPrice')} sortConfig={sortConfig} - title={t('limit-price')} + title={t('trade:limit-price')} />
@@ -196,16 +204,17 @@ const SwapOrders = () => { const { buyBank, fee, - market, + pair, orderType, limitPrice, sellBank, size, + filled, triggerPrice, } = data return ( - {market} + {pair}

{t(orderType)}

@@ -219,7 +228,16 @@ const SwapOrders = () => {

- {triggerPrice !== limitPrice ? ( +

+ {filled}/{size} + + {' '} + {sellBank.name} + +

+ + + {triggerPrice ? (

{triggerPrice} diff --git a/components/swap/SwapPage.tsx b/components/swap/SwapPage.tsx index 97bedd9c..0e980709 100644 --- a/components/swap/SwapPage.tsx +++ b/components/swap/SwapPage.tsx @@ -34,10 +34,10 @@ const SwapPage = () => { return ( <>

-
+
-
+
diff --git a/public/locales/en/swap.json b/public/locales/en/swap.json index 2b46ba37..5408cabe 100644 --- a/public/locales/en/swap.json +++ b/public/locales/en/swap.json @@ -20,6 +20,7 @@ "no-swap-found": "No swap found", "output-reduce-only-warning": "{{symbol}} is in reduce only mode. You can swap to close borrows only", "paid": "Paid", + "pair": "Pair", "place-limit-order": "Place Limit Order", "preset": "Preset", "price-impact": "Price Impact", diff --git a/public/locales/en/trade.json b/public/locales/en/trade.json index 161af2b1..d6ca1af3 100644 --- a/public/locales/en/trade.json +++ b/public/locales/en/trade.json @@ -21,6 +21,7 @@ "est-liq-price": "Est. Liq. Price", "avg-entry-price": "Avg. Entry Price", "est-slippage": "Est. Slippage", + "filled": "Filled", "for": "for", "funding-limits": "Funding Limits", "funding-rate": "1h Avg Funding Rate", diff --git a/public/locales/es/swap.json b/public/locales/es/swap.json index 2b46ba37..5408cabe 100644 --- a/public/locales/es/swap.json +++ b/public/locales/es/swap.json @@ -20,6 +20,7 @@ "no-swap-found": "No swap found", "output-reduce-only-warning": "{{symbol}} is in reduce only mode. You can swap to close borrows only", "paid": "Paid", + "pair": "Pair", "place-limit-order": "Place Limit Order", "preset": "Preset", "price-impact": "Price Impact", diff --git a/public/locales/es/trade.json b/public/locales/es/trade.json index 161af2b1..d6ca1af3 100644 --- a/public/locales/es/trade.json +++ b/public/locales/es/trade.json @@ -21,6 +21,7 @@ "est-liq-price": "Est. Liq. Price", "avg-entry-price": "Avg. Entry Price", "est-slippage": "Est. Slippage", + "filled": "Filled", "for": "for", "funding-limits": "Funding Limits", "funding-rate": "1h Avg Funding Rate", diff --git a/public/locales/ru/swap.json b/public/locales/ru/swap.json index 2b46ba37..5408cabe 100644 --- a/public/locales/ru/swap.json +++ b/public/locales/ru/swap.json @@ -20,6 +20,7 @@ "no-swap-found": "No swap found", "output-reduce-only-warning": "{{symbol}} is in reduce only mode. You can swap to close borrows only", "paid": "Paid", + "pair": "Pair", "place-limit-order": "Place Limit Order", "preset": "Preset", "price-impact": "Price Impact", diff --git a/public/locales/ru/trade.json b/public/locales/ru/trade.json index 161af2b1..d6ca1af3 100644 --- a/public/locales/ru/trade.json +++ b/public/locales/ru/trade.json @@ -21,6 +21,7 @@ "est-liq-price": "Est. Liq. Price", "avg-entry-price": "Avg. Entry Price", "est-slippage": "Est. Slippage", + "filled": "Filled", "for": "for", "funding-limits": "Funding Limits", "funding-rate": "1h Avg Funding Rate", diff --git a/public/locales/zh/swap.json b/public/locales/zh/swap.json index 61f47082..d9f11334 100644 --- a/public/locales/zh/swap.json +++ b/public/locales/zh/swap.json @@ -20,6 +20,7 @@ "no-swap-found": "查不到换币", "output-reduce-only-warning": "{{symbol}}处于仅减少模式。换币限于归还借贷", "paid": "付出", + "pair": "Pair", "pay": "你将付出", "place-limit-order": "Place Limit Order", "preset": "预设", diff --git a/public/locales/zh/trade.json b/public/locales/zh/trade.json index 4c5c4efc..106553ff 100644 --- a/public/locales/zh/trade.json +++ b/public/locales/zh/trade.json @@ -21,6 +21,7 @@ "est-liq-price": "Est. Liq. Price", "avg-entry-price": "Avg. Entry Price", "est-slippage": "Est. Slippage", + "filled": "Filled", "for": "for", "funding-limits": "Funding Limits", "funding-rate": "1h Avg Funding Rate", diff --git a/public/locales/zh_tw/swap.json b/public/locales/zh_tw/swap.json index ba17a376..5b39668b 100644 --- a/public/locales/zh_tw/swap.json +++ b/public/locales/zh_tw/swap.json @@ -20,6 +20,7 @@ "no-swap-found": "查不到換幣", "output-reduce-only-warning": "{{symbol}}處於僅減少模式。換幣限於歸還借貸", "paid": "付出", + "pair": "Pair", "pay": "你將付出", "place-limit-order": "Place Limit Order", "preset": "預設", diff --git a/public/locales/zh_tw/trade.json b/public/locales/zh_tw/trade.json index 2069eb98..cd0418f0 100644 --- a/public/locales/zh_tw/trade.json +++ b/public/locales/zh_tw/trade.json @@ -21,6 +21,7 @@ "edit-order": "編輯訂單", "est-liq-price": "預計清算價格", "est-slippage": "預計下滑", + "filled": "Filled", "for": "for", "funding-limits": "資金費限制", "funding-rate": "1小時平均資金費", From 36484b0dd5afd4a884ea00b93be33b02daa2cdd6 Mon Sep 17 00:00:00 2001 From: saml33 Date: Mon, 31 Jul 2023 22:28:58 +1000 Subject: [PATCH 08/44] bump client --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 936b15e2..2a05443a 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ }, "dependencies": { "@blockworks-foundation/mango-feeds": "0.1.7", - "@blockworks-foundation/mango-v4": "^0.18.3", + "@blockworks-foundation/mango-v4": "^0.18.5", "@headlessui/react": "1.6.6", "@heroicons/react": "2.0.10", "@metaplex-foundation/js": "0.19.4", diff --git a/yarn.lock b/yarn.lock index 49221279..3e3a5ebb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,7 +12,7 @@ resolved "https://registry.yarnpkg.com/@apocentre/alias-sampling/-/alias-sampling-0.5.3.tgz#897ff181b48ad7b2bcb4ecf29400214888244f08" integrity sha512-7UDWIIF9hIeJqfKXkNIzkVandlwLf1FWTSdrb9iXvOP8oF544JRXQjCbiTmCv2c9n44n/FIWtehhBfNuAx2CZA== -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.5", "@babel/runtime@^7.12.5", "@babel/runtime@^7.17.2", "@babel/runtime@^7.19.4", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.7", "@babel/runtime@^7.22.5", "@babel/runtime@^7.22.6": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.5", "@babel/runtime@^7.17.2", "@babel/runtime@^7.19.4", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.7", "@babel/runtime@^7.22.5", "@babel/runtime@^7.22.6": version "7.22.6" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.6.tgz#57d64b9ae3cff1d67eb067ae117dac087f5bd438" integrity sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ== @@ -26,10 +26,10 @@ dependencies: ws "^8.13.0" -"@blockworks-foundation/mango-v4@^0.18.3": - version "0.18.3" - resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4/-/mango-v4-0.18.3.tgz#a3d41cfb85f9b7121469d2ac3d52a7915f677701" - integrity sha512-j45GXiLPncKaSnBPLg0JLYEW2kSRRCWklE0usmhPlPD99KcX/iKqwFEzWtcOuGC5sQJTn4cblTOj5V6iiUiGAw== +"@blockworks-foundation/mango-v4@0.18.5": + version "0.18.5" + resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4/-/mango-v4-0.18.5.tgz#dec488ea957b78dabe61b05049e6f65ebe45ae8f" + integrity sha512-SF4qboOFAQ+pWmzDnNdboDQN6DTLkVR82Qm9SSkTiQRUfHi9gqEaF6QlJkjzBcECiqk6FB9Xg9rOZb4OKAssNw== dependencies: "@coral-xyz/anchor" "^0.27.0" "@project-serum/serum" "0.13.65" From f29718610aa47aeb24f5737ce82497eddc01ef7c Mon Sep 17 00:00:00 2001 From: saml33 Date: Mon, 31 Jul 2023 22:42:56 +1000 Subject: [PATCH 09/44] yarn setup --- yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 3e3a5ebb..d22c1b1d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26,7 +26,7 @@ dependencies: ws "^8.13.0" -"@blockworks-foundation/mango-v4@0.18.5": +"@blockworks-foundation/mango-v4@^0.18.5": version "0.18.5" resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4/-/mango-v4-0.18.5.tgz#dec488ea957b78dabe61b05049e6f65ebe45ae8f" integrity sha512-SF4qboOFAQ+pWmzDnNdboDQN6DTLkVR82Qm9SSkTiQRUfHi9gqEaF6QlJkjzBcECiqk6FB9Xg9rOZb4OKAssNw== From c35ccd54aed24d40682427e5142be7cf13841c43 Mon Sep 17 00:00:00 2001 From: saml33 Date: Tue, 1 Aug 2023 10:57:53 +1000 Subject: [PATCH 10/44] form validation and copy --- components/swap/LimitSwapForm.tsx | 243 +++++++++++++++++++++++------- components/swap/SwapForm.tsx | 2 +- components/swap/SwapInfoTabs.tsx | 4 +- public/locales/en/swap.json | 2 +- public/locales/en/trade.json | 2 + public/locales/es/swap.json | 2 +- public/locales/es/trade.json | 2 + public/locales/ru/swap.json | 2 +- public/locales/ru/trade.json | 2 + public/locales/zh/swap.json | 2 +- public/locales/zh/trade.json | 2 + public/locales/zh_tw/swap.json | 2 +- public/locales/zh_tw/trade.json | 2 + 13 files changed, 209 insertions(+), 60 deletions(-) diff --git a/components/swap/LimitSwapForm.tsx b/components/swap/LimitSwapForm.tsx index 9d67e0cc..5f9adf5e 100644 --- a/components/swap/LimitSwapForm.tsx +++ b/components/swap/LimitSwapForm.tsx @@ -30,11 +30,18 @@ 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' +import InlineNotification from '@components/shared/InlineNotification' type LimitSwapFormProps = { setShowTokenSelect: Dispatch> } +type LimitSwapForm = { + limitPrice: string | undefined + triggerPrice: string +} +type FormErrors = Partial> + const ORDER_TYPES = [ // 'trade:limit', 'trade:stop-market', @@ -51,6 +58,7 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { const [triggerPrice, setTriggerPrice] = useState('') const [submitting, setSubmitting] = useState(false) const [swapFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider') + const [formErrors, setFormErrors] = useState({}) const { margin: useMargin, @@ -107,19 +115,75 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { }) }, []) + const initialQuotePrice = useMemo(() => { + if (!baseBank || !quoteBank) return + return baseBank.uiPrice / quoteBank.uiPrice + }, [baseBank, quoteBank]) + // set default limit and trigger price useEffect(() => { - if (!baseBank || !quoteBank) return - const initialPrice = baseBank.uiPrice / quoteBank.uiPrice + if (!initialQuotePrice) return if (!triggerPrice) { - setTriggerPrice((initialPrice * 0.9).toString()) + setTriggerPrice((initialQuotePrice * 0.9).toString()) } if (!limitPrice) { set((s) => { - s.swap.limitPrice = (initialPrice * 0.8).toString() + s.swap.limitPrice = (initialQuotePrice * 0.8).toString() }) } - }, [baseBank, limitPrice, quoteBank, triggerPrice]) + }, [initialQuotePrice, limitPrice, triggerPrice]) + + const [limitPriceDifference, triggerPriceDifference] = useMemo(() => { + if (!initialQuotePrice) return [0, 0] + const limitDifference = limitPrice + ? ((parseFloat(limitPrice) - initialQuotePrice) / initialQuotePrice) * 100 + : 0 + const triggerDifference = triggerPrice + ? ((parseFloat(triggerPrice) - initialQuotePrice) / initialQuotePrice) * + 100 + : 0 + return [limitDifference, triggerDifference] + }, [initialQuotePrice, limitPrice, triggerPrice]) + + const isFormValid = useCallback( + (form: LimitSwapForm) => { + const invalidFields: FormErrors = {} + setFormErrors({}) + const triggerPriceNumber = parseFloat(form.triggerPrice) + const requiredFields: (keyof LimitSwapForm)[] = [ + 'limitPrice', + 'triggerPrice', + ] + for (const key of requiredFields) { + const value = form[key] as string + if (!value) { + if (orderType === 'trade:stop-market') { + if (key !== 'limitPrice') { + invalidFields[key] = t('settings:error-required-field') + } + } else { + invalidFields[key] = t('settings:error-required-field') + } + } + } + if ( + orderType.includes('stop') && + initialQuotePrice && + triggerPriceNumber > initialQuotePrice + ) { + 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 @@ -133,15 +197,17 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { (e: NumberFormatValues, info: SourceInfo) => { if (info.source !== 'event') return setAmountInFormValue(e.value) - if (parseFloat(e.value) > 0 && limitPrice && outputBank) { + const price = + orderType === 'trade:stop-market' ? triggerPrice : limitPrice + if (parseFloat(e.value) > 0 && price && outputBank) { const amount = outputBank.name === quoteBank?.name ? floorToDecimal( - parseFloat(e.value) * parseFloat(limitPrice), + parseFloat(e.value) * parseFloat(price), outputBank.mintDecimals, ) : floorToDecimal( - parseFloat(e.value) / parseFloat(limitPrice), + parseFloat(e.value) / parseFloat(price), outputBank.mintDecimals, ) setAmountOutFormValue(amount.toString()) @@ -149,10 +215,12 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { }, [ limitPrice, + orderType, outputBank, quoteBank, setAmountInFormValue, setAmountOutFormValue, + triggerPrice, ], ) @@ -160,15 +228,17 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { (e: NumberFormatValues, info: SourceInfo) => { if (info.source !== 'event') return setAmountOutFormValue(e.value) - if (parseFloat(e.value) > 0 && limitPrice && inputBank) { + const price = + orderType === 'trade:stop-market' ? triggerPrice : limitPrice + if (parseFloat(e.value) > 0 && price && inputBank) { const amount = outputBank?.name === quoteBank?.name ? floorToDecimal( - parseFloat(e.value) / parseFloat(limitPrice), + parseFloat(e.value) / parseFloat(price), inputBank.mintDecimals, ) : floorToDecimal( - parseFloat(e.value) * parseFloat(limitPrice), + parseFloat(e.value) * parseFloat(price), inputBank.mintDecimals, ) setAmountInFormValue(amount.toString()) @@ -176,10 +246,12 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { }, [ inputBank, + orderType, outputBank, limitPrice, setAmountInFormValue, setAmountOutFormValue, + triggerPrice, ], ) @@ -200,31 +272,90 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { const handleLimitPrice = useCallback( (e: NumberFormatValues, info: SourceInfo) => { if (info.source !== 'event') return + setFormErrors({}) setLimitPrice(e.value) - if ( - parseFloat(e.value) > 0 && - parseFloat(amountInFormValue) > 0 && - outputBank - ) { - const amount = floorToDecimal( - parseFloat(amountInFormValue) * parseFloat(e.value), - outputBank.mintDecimals, - ) + const triggerPriceNumber = parseFloat(e.value) + const amountInNumber = parseFloat(amountInFormValue) + if (triggerPriceNumber > 0 && amountInNumber > 0 && outputBank) { + const amount = + outputBank?.name === quoteBank?.name + ? floorToDecimal( + amountInNumber * triggerPriceNumber, + outputBank.mintDecimals, + ) + : floorToDecimal( + amountInNumber / triggerPriceNumber, + outputBank.mintDecimals, + ) setAmountOutFormValue(amount.toString()) } }, - [amountInFormValue, outputBank, setLimitPrice], + [amountInFormValue, outputBank, quoteBank, setLimitPrice], ) const handleTriggerPrice = useCallback( (e: NumberFormatValues, info: SourceInfo) => { if (info.source !== 'event') return + setFormErrors({}) setTriggerPrice(e.value) + const triggerPriceNumber = parseFloat(e.value) + const amountInNumber = parseFloat(amountInFormValue) + if ( + triggerPriceNumber > 0 && + amountInNumber > 0 && + outputBank && + orderType === 'trade:stop-market' + ) { + const amount = + outputBank?.name === quoteBank?.name + ? floorToDecimal( + amountInNumber * triggerPriceNumber, + outputBank.mintDecimals, + ) + : floorToDecimal( + amountInNumber / triggerPriceNumber, + outputBank.mintDecimals, + ) + setAmountOutFormValue(amount.toString()) + } }, - [setTriggerPrice], + [amountInFormValue, orderType, outputBank, quoteBank, setTriggerPrice], ) - const handleLimitSwap = useCallback(async () => { + const handleSwitchTokens = useCallback(() => { + const price = orderType === 'trade:stop-market' ? triggerPrice : limitPrice + if (amountInAsDecimal?.gt(0) && price) { + const amountOut = + outputBank?.name !== quoteBank?.name + ? amountInAsDecimal.mul(price) + : amountInAsDecimal.div(price) + 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, + orderType, + outputBank, + quoteBank, + triggerPrice, + ]) + + const handlePlaceStopLoss = useCallback(async () => { + const invalidFields = isFormValid({ limitPrice, triggerPrice }) + if (Object.keys(invalidFields).length) { + return + } try { const client = mangoStore.getState().client const group = mangoStore.getState().group @@ -291,32 +422,6 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { } }, [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 @@ -351,7 +456,14 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => {
{orderType !== 'trade:limit' ? (
-

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

+

+ {t('trade:trigger-price')}{' '} + + {triggerPriceDifference + ? `(${triggerPriceDifference.toFixed(2)}%)` + : ''} + +

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

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

+

+ {t('trade:limit-price')}{' '} + + {limitPriceDifference + ? `(${limitPriceDifference.toFixed(2)}%)` + : ''} + +

{
+ {formErrors.limitPrice ? ( +
+ +
+ ) : null}
) : null}
@@ -433,7 +572,7 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { /> )}
{selectedTab === 'balances' ? : null} - {selectedTab === 'trade:orders' ? : null} + {selectedTab === 'trade:stop-orders' ? : null} {selectedTab === 'swap:swap-history' ? : null}
) diff --git a/public/locales/en/swap.json b/public/locales/en/swap.json index 5408cabe..a087f46d 100644 --- a/public/locales/en/swap.json +++ b/public/locales/en/swap.json @@ -21,7 +21,7 @@ "output-reduce-only-warning": "{{symbol}} is in reduce only mode. You can swap to close borrows only", "paid": "Paid", "pair": "Pair", - "place-limit-order": "Place Limit Order", + "place-limit-order": "Place Order", "preset": "Preset", "price-impact": "Price Impact", "rate": "Rate", diff --git a/public/locales/en/trade.json b/public/locales/en/trade.json index d6ca1af3..b2885119 100644 --- a/public/locales/en/trade.json +++ b/public/locales/en/trade.json @@ -84,7 +84,9 @@ "spread": "Spread", "stable-price": "Stable Price", "stop-limit": "Stop Limit", + "stop-loss": "Stop-loss", "stop-market": "Stop Market", + "stop-orders": "Stop Orders", "taker": "Taker", "taker-fee": "Taker Fee", "tick-size": "Tick Size", diff --git a/public/locales/es/swap.json b/public/locales/es/swap.json index 5408cabe..a087f46d 100644 --- a/public/locales/es/swap.json +++ b/public/locales/es/swap.json @@ -21,7 +21,7 @@ "output-reduce-only-warning": "{{symbol}} is in reduce only mode. You can swap to close borrows only", "paid": "Paid", "pair": "Pair", - "place-limit-order": "Place Limit Order", + "place-limit-order": "Place Order", "preset": "Preset", "price-impact": "Price Impact", "rate": "Rate", diff --git a/public/locales/es/trade.json b/public/locales/es/trade.json index d6ca1af3..b2885119 100644 --- a/public/locales/es/trade.json +++ b/public/locales/es/trade.json @@ -84,7 +84,9 @@ "spread": "Spread", "stable-price": "Stable Price", "stop-limit": "Stop Limit", + "stop-loss": "Stop-loss", "stop-market": "Stop Market", + "stop-orders": "Stop Orders", "taker": "Taker", "taker-fee": "Taker Fee", "tick-size": "Tick Size", diff --git a/public/locales/ru/swap.json b/public/locales/ru/swap.json index 5408cabe..a087f46d 100644 --- a/public/locales/ru/swap.json +++ b/public/locales/ru/swap.json @@ -21,7 +21,7 @@ "output-reduce-only-warning": "{{symbol}} is in reduce only mode. You can swap to close borrows only", "paid": "Paid", "pair": "Pair", - "place-limit-order": "Place Limit Order", + "place-limit-order": "Place Order", "preset": "Preset", "price-impact": "Price Impact", "rate": "Rate", diff --git a/public/locales/ru/trade.json b/public/locales/ru/trade.json index d6ca1af3..b2885119 100644 --- a/public/locales/ru/trade.json +++ b/public/locales/ru/trade.json @@ -84,7 +84,9 @@ "spread": "Spread", "stable-price": "Stable Price", "stop-limit": "Stop Limit", + "stop-loss": "Stop-loss", "stop-market": "Stop Market", + "stop-orders": "Stop Orders", "taker": "Taker", "taker-fee": "Taker Fee", "tick-size": "Tick Size", diff --git a/public/locales/zh/swap.json b/public/locales/zh/swap.json index d9f11334..5eb69360 100644 --- a/public/locales/zh/swap.json +++ b/public/locales/zh/swap.json @@ -22,7 +22,7 @@ "paid": "付出", "pair": "Pair", "pay": "你将付出", - "place-limit-order": "Place Limit Order", + "place-limit-order": "Place Order", "preset": "预设", "price-impact": "价格影响", "rate": "汇率", diff --git a/public/locales/zh/trade.json b/public/locales/zh/trade.json index 106553ff..af10e6ad 100644 --- a/public/locales/zh/trade.json +++ b/public/locales/zh/trade.json @@ -83,7 +83,9 @@ "spread": "差價", "stable-price": "穩定價格", "stop-limit": "Stop Limit", + "stop-loss": "Stop-loss", "stop-market": "Stop Market", + "stop-orders": "Stop Orders", "taker": "吃單者", "tick-size": "波動單位", "taker-fee": "Taker Fee", diff --git a/public/locales/zh_tw/swap.json b/public/locales/zh_tw/swap.json index 5b39668b..8b22e38a 100644 --- a/public/locales/zh_tw/swap.json +++ b/public/locales/zh_tw/swap.json @@ -22,7 +22,7 @@ "paid": "付出", "pair": "Pair", "pay": "你將付出", - "place-limit-order": "Place Limit Order", + "place-limit-order": "Place Order", "preset": "預設", "price-impact": "價格影響", "rate": "匯率", diff --git a/public/locales/zh_tw/trade.json b/public/locales/zh_tw/trade.json index cd0418f0..bee6cf5b 100644 --- a/public/locales/zh_tw/trade.json +++ b/public/locales/zh_tw/trade.json @@ -84,7 +84,9 @@ "spread": "差價", "stable-price": "穩定價格", "stop-limit": "Stop Limit", + "stop-loss": "Stop-loss", "stop-market": "Stop Market", + "stop-orders": "Stop Orders", "taker": "吃單者", "taker-fee": "吃單者費用", "tick-size": "波動單位", From 9013cd8722fe42a609ea87f4193ddcb17ce48e68 Mon Sep 17 00:00:00 2001 From: saml33 Date: Tue, 1 Aug 2023 15:13:12 +1000 Subject: [PATCH 11/44] fix limit and trigger price decimals --- components/swap/LimitSwapForm.tsx | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/components/swap/LimitSwapForm.tsx b/components/swap/LimitSwapForm.tsx index 5f9adf5e..54429bf1 100644 --- a/components/swap/LimitSwapForm.tsx +++ b/components/swap/LimitSwapForm.tsx @@ -117,30 +117,37 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { const initialQuotePrice = useMemo(() => { if (!baseBank || !quoteBank) return - return baseBank.uiPrice / quoteBank.uiPrice + return floorToDecimal( + baseBank.uiPrice / quoteBank.uiPrice, + quoteBank.mintDecimals, + ) }, [baseBank, quoteBank]) // set default limit and trigger price useEffect(() => { if (!initialQuotePrice) return if (!triggerPrice) { - setTriggerPrice((initialQuotePrice * 0.9).toString()) + setTriggerPrice( + initialQuotePrice.mul(0.9).toFixed(quoteBank?.mintDecimals), + ) } if (!limitPrice) { set((s) => { - s.swap.limitPrice = (initialQuotePrice * 0.8).toString() + s.swap.limitPrice = initialQuotePrice + .mul(0.8) + .toFixed(quoteBank?.mintDecimals) }) } - }, [initialQuotePrice, limitPrice, triggerPrice]) + }, [initialQuotePrice, limitPrice, quoteBank, triggerPrice]) const [limitPriceDifference, triggerPriceDifference] = useMemo(() => { if (!initialQuotePrice) return [0, 0] + const initialPrice = initialQuotePrice.toNumber() const limitDifference = limitPrice - ? ((parseFloat(limitPrice) - initialQuotePrice) / initialQuotePrice) * 100 + ? ((parseFloat(limitPrice) - initialPrice) / initialPrice) * 100 : 0 const triggerDifference = triggerPrice - ? ((parseFloat(triggerPrice) - initialQuotePrice) / initialQuotePrice) * - 100 + ? ((parseFloat(triggerPrice) - initialPrice) / initialPrice) * 100 : 0 return [limitDifference, triggerDifference] }, [initialQuotePrice, limitPrice, triggerPrice]) @@ -169,7 +176,7 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { if ( orderType.includes('stop') && initialQuotePrice && - triggerPriceNumber > initialQuotePrice + triggerPriceNumber > initialQuotePrice.toNumber() ) { invalidFields.triggerPrice = 'Trigger price must be less than current price' From 83662a52ebb08dd2986cdc4f8f3377a8335ec145 Mon Sep 17 00:00:00 2001 From: saml33 Date: Tue, 1 Aug 2023 20:49:01 +1000 Subject: [PATCH 12/44] remove validation for testing --- components/swap/LimitSwapForm.tsx | 223 ++++++++++++++---------------- components/swap/SwapInfoTabs.tsx | 9 +- 2 files changed, 111 insertions(+), 121 deletions(-) diff --git a/components/swap/LimitSwapForm.tsx b/components/swap/LimitSwapForm.tsx index 54429bf1..d1fe7891 100644 --- a/components/swap/LimitSwapForm.tsx +++ b/components/swap/LimitSwapForm.tsx @@ -75,12 +75,6 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { : 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] @@ -152,45 +146,45 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { return [limitDifference, triggerDifference] }, [initialQuotePrice, limitPrice, triggerPrice]) - const isFormValid = useCallback( - (form: LimitSwapForm) => { - const invalidFields: FormErrors = {} - setFormErrors({}) - const triggerPriceNumber = parseFloat(form.triggerPrice) - const requiredFields: (keyof LimitSwapForm)[] = [ - 'limitPrice', - 'triggerPrice', - ] - for (const key of requiredFields) { - const value = form[key] as string - if (!value) { - if (orderType === 'trade:stop-market') { - if (key !== 'limitPrice') { - invalidFields[key] = t('settings:error-required-field') - } - } else { - invalidFields[key] = t('settings:error-required-field') - } - } - } - 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], - ) + // const isFormValid = useCallback( + // (form: LimitSwapForm) => { + // const invalidFields: FormErrors = {} + // setFormErrors({}) + // const triggerPriceNumber = parseFloat(form.triggerPrice) + // const requiredFields: (keyof LimitSwapForm)[] = [ + // 'limitPrice', + // 'triggerPrice', + // ] + // for (const key of requiredFields) { + // const value = form[key] as string + // if (!value) { + // if (orderType === 'trade:stop-market') { + // if (key !== 'limitPrice') { + // invalidFields[key] = t('settings:error-required-field') + // } + // } else { + // invalidFields[key] = t('settings:error-required-field') + // } + // } + // } + // 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 @@ -200,31 +194,56 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { setAmountOutFormValue('') }, [useMargin, setAmountInFormValue, setAmountOutFormValue]) + // get the out amount from the in amount and trigger or limit price + const getAmountOut = useCallback( + (amountIn: string, price: string) => { + const amountOut = + outputBank?.name === quoteBank?.name + ? floorToDecimal( + parseFloat(amountIn) * parseFloat(price), + outputBank?.mintDecimals || 0, + ) + : floorToDecimal( + parseFloat(amountIn) / parseFloat(price), + outputBank?.mintDecimals || 0, + ) + return amountOut + }, + [outputBank, quoteBank], + ) + + // get the in amount from the out amount and trigger or limit price + const getAmountIn = useCallback( + (amountOut: string, price: string) => { + const amountIn = + outputBank?.name === quoteBank?.name + ? floorToDecimal( + parseFloat(amountOut) / parseFloat(price), + inputBank?.mintDecimals || 0, + ) + : floorToDecimal( + parseFloat(amountOut) * parseFloat(price), + inputBank?.mintDecimals || 0, + ) + return amountIn + }, + [inputBank, outputBank, quoteBank], + ) + const handleAmountInChange = useCallback( (e: NumberFormatValues, info: SourceInfo) => { if (info.source !== 'event') return setAmountInFormValue(e.value) const price = orderType === 'trade:stop-market' ? triggerPrice : limitPrice - if (parseFloat(e.value) > 0 && price && outputBank) { - const amount = - outputBank.name === quoteBank?.name - ? floorToDecimal( - parseFloat(e.value) * parseFloat(price), - outputBank.mintDecimals, - ) - : floorToDecimal( - parseFloat(e.value) / parseFloat(price), - outputBank.mintDecimals, - ) - setAmountOutFormValue(amount.toString()) + if (parseFloat(e.value) > 0 && price) { + const amountOut = getAmountOut(e.value, price) + setAmountOutFormValue(amountOut.toString()) } }, [ limitPrice, orderType, - outputBank, - quoteBank, setAmountInFormValue, setAmountOutFormValue, triggerPrice, @@ -237,24 +256,13 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { setAmountOutFormValue(e.value) const price = orderType === 'trade:stop-market' ? triggerPrice : limitPrice - if (parseFloat(e.value) > 0 && price && inputBank) { - const amount = - outputBank?.name === quoteBank?.name - ? floorToDecimal( - parseFloat(e.value) / parseFloat(price), - inputBank.mintDecimals, - ) - : floorToDecimal( - parseFloat(e.value) * parseFloat(price), - inputBank.mintDecimals, - ) - setAmountInFormValue(amount.toString()) + if (parseFloat(e.value) > 0 && price) { + const amountIn = getAmountIn(e.value, price) + setAmountInFormValue(amountIn.toString()) } }, [ - inputBank, orderType, - outputBank, limitPrice, setAmountInFormValue, setAmountOutFormValue, @@ -265,15 +273,20 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { const handleAmountInUi = useCallback( (amountIn: string) => { setAmountInFormValue(amountIn) - if (limitPrice && outputBank) { - const amount = floorToDecimal( - parseFloat(amountIn) / parseFloat(limitPrice), - outputBank.mintDecimals, - ) - setAmountOutFormValue(amount.toString()) + const price = + orderType === 'trade:stop-market' ? triggerPrice : limitPrice + if (price) { + const amountOut = getAmountOut(amountIn, price) + setAmountOutFormValue(amountOut.toString()) } }, - [limitPrice, outputBank, setAmountInFormValue, setAmountOutFormValue], + [ + limitPrice, + orderType, + setAmountInFormValue, + setAmountOutFormValue, + triggerPrice, + ], ) const handleLimitPrice = useCallback( @@ -281,23 +294,12 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { if (info.source !== 'event') return setFormErrors({}) setLimitPrice(e.value) - const triggerPriceNumber = parseFloat(e.value) - const amountInNumber = parseFloat(amountInFormValue) - if (triggerPriceNumber > 0 && amountInNumber > 0 && outputBank) { - const amount = - outputBank?.name === quoteBank?.name - ? floorToDecimal( - amountInNumber * triggerPriceNumber, - outputBank.mintDecimals, - ) - : floorToDecimal( - amountInNumber / triggerPriceNumber, - outputBank.mintDecimals, - ) - setAmountOutFormValue(amount.toString()) + if (parseFloat(e.value) > 0 && parseFloat(amountInFormValue) > 0) { + const amountOut = getAmountOut(amountInFormValue, e.value) + setAmountOutFormValue(amountOut.toString()) } }, - [amountInFormValue, outputBank, quoteBank, setLimitPrice], + [amountInFormValue, setLimitPrice], ) const handleTriggerPrice = useCallback( @@ -305,28 +307,16 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { if (info.source !== 'event') return setFormErrors({}) setTriggerPrice(e.value) - const triggerPriceNumber = parseFloat(e.value) - const amountInNumber = parseFloat(amountInFormValue) if ( - triggerPriceNumber > 0 && - amountInNumber > 0 && - outputBank && + parseFloat(e.value) > 0 && + parseFloat(amountInFormValue) > 0 && orderType === 'trade:stop-market' ) { - const amount = - outputBank?.name === quoteBank?.name - ? floorToDecimal( - amountInNumber * triggerPriceNumber, - outputBank.mintDecimals, - ) - : floorToDecimal( - amountInNumber / triggerPriceNumber, - outputBank.mintDecimals, - ) - setAmountOutFormValue(amount.toString()) + const amountOut = getAmountOut(amountInFormValue, e.value) + setAmountOutFormValue(amountOut.toString()) } }, - [amountInFormValue, orderType, outputBank, quoteBank, setTriggerPrice], + [amountInFormValue, orderType, setTriggerPrice], ) const handleSwitchTokens = useCallback(() => { @@ -348,7 +338,6 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { ) }, [ setAmountInFormValue, - amountOutAsDecimal, amountInAsDecimal, limitPrice, inputBank, @@ -359,10 +348,10 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { ]) const handlePlaceStopLoss = useCallback(async () => { - const invalidFields = isFormValid({ limitPrice, triggerPrice }) - if (Object.keys(invalidFields).length) { - return - } + // const invalidFields = isFormValid({ limitPrice, triggerPrice }) + // if (Object.keys(invalidFields).length) { + // return + // } try { const client = mangoStore.getState().client const group = mangoStore.getState().group diff --git a/components/swap/SwapInfoTabs.tsx b/components/swap/SwapInfoTabs.tsx index bafe5c16..624bcfdd 100644 --- a/components/swap/SwapInfoTabs.tsx +++ b/components/swap/SwapInfoTabs.tsx @@ -1,7 +1,6 @@ import { useMemo, useState } from 'react' import TabButtons from '@components/shared/TabButtons' import SwapTradeBalances from '../shared/BalancesTable' -import mangoStore from '@store/mangoStore' import SwapHistoryTable from './SwapHistoryTable' import useMangoAccount from 'hooks/useMangoAccount' import ManualRefresh from '@components/shared/ManualRefresh' @@ -11,18 +10,20 @@ import SwapOrders from './SwapOrders' const SwapInfoTabs = () => { const [selectedTab, setSelectedTab] = useState('balances') - const openOrders = mangoStore((s) => s.mangoAccount.openOrders) const { mangoAccount } = useMangoAccount() const { width } = useViewport() const isMobile = width ? width < breakpoints.lg : false const tabsWithCount: [string, number][] = useMemo(() => { + const stopOrdersCount = + mangoAccount?.tokenConditionalSwaps.filter((tcs) => tcs.hasData) + ?.length || 0 return [ ['balances', 0], - ['trade:stop-orders', 0], + ['trade:stop-orders', stopOrdersCount], ['swap:swap-history', 0], ] - }, [openOrders, mangoAccount]) + }, [mangoAccount]) return (
From f284f45afd3588501eb19c39f5f07f1745387297 Mon Sep 17 00:00:00 2001 From: saml33 Date: Tue, 1 Aug 2023 22:21:19 +1000 Subject: [PATCH 13/44] set sizes from bal/max/repay buttons --- components/swap/BuyTokenInput.tsx | 30 +++-------- components/swap/LimitSwapForm.tsx | 83 +++++++++++++++--------------- components/swap/MarketSwapForm.tsx | 40 ++++++++++---- components/swap/SellTokenInput.tsx | 6 +-- 4 files changed, 82 insertions(+), 77 deletions(-) diff --git a/components/swap/BuyTokenInput.tsx b/components/swap/BuyTokenInput.tsx index 79eaa711..d4c1e79d 100644 --- a/components/swap/BuyTokenInput.tsx +++ b/components/swap/BuyTokenInput.tsx @@ -7,32 +7,28 @@ import NumberFormat, { } from 'react-number-format' import { formatCurrencyValue } from 'utils/numbers' import { useTranslation } from 'react-i18next' -import { Dispatch, SetStateAction, useCallback, useMemo } from 'react' +import { Dispatch, SetStateAction, useMemo } from 'react' import mangoStore from '@store/mangoStore' import useMangoGroup from 'hooks/useMangoGroup' import { OUTPUT_TOKEN_DEFAULT } from 'utils/constants' import { NUMBER_FORMAT_CLASSNAMES } from './MarketSwapForm' -const set = mangoStore.getState().set - const BuyTokenInput = ({ handleAmountOutChange, loading, setShowTokenSelect, - setAmountOutFormValue, + handleRepay, }: { handleAmountOutChange: (e: NumberFormatValues, info: SourceInfo) => void loading?: boolean setShowTokenSelect: Dispatch> - setAmountOutFormValue: (amountOut: string) => void + handleRepay: (amountOut: string) => void }) => { const { t } = useTranslation('common') const { group } = useMangoGroup() - const { - outputBank, - amountOut: amountOutFormValue, - swapMode, - } = mangoStore((s) => s.swap) + const { outputBank, amountOut: amountOutFormValue } = mangoStore( + (s) => s.swap, + ) const outputTokenBalanceBorrow = useMemo(() => { if (!outputBank) return 0 @@ -41,18 +37,6 @@ const BuyTokenInput = ({ return balance && balance < 0 ? Math.abs(balance) : 0 }, [outputBank]) - const setBorrowAmountOut = useCallback( - (borrowAmount: string) => { - if (swapMode === 'ExactIn') { - set((s) => { - s.swap.swapMode = 'ExactOut' - }) - } - setAmountOutFormValue(borrowAmount.toString()) - }, - [setAmountOutFormValue], - ) - return (
@@ -63,7 +47,7 @@ const BuyTokenInput = ({ decimals={outputBank?.mintDecimals || 9} label={t('repay')} onClick={() => - setBorrowAmountOut( + handleRepay( outputTokenBalanceBorrow.toFixed(outputBank?.mintDecimals || 9), ) } diff --git a/components/swap/LimitSwapForm.tsx b/components/swap/LimitSwapForm.tsx index d1fe7891..08818f5f 100644 --- a/components/swap/LimitSwapForm.tsx +++ b/components/swap/LimitSwapForm.tsx @@ -194,6 +194,11 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { 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 const getAmountOut = useCallback( (amountIn: string, price: string) => { @@ -230,63 +235,61 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { [inputBank, outputBank, quoteBank], ) + const handleMax = useCallback( + (amountIn: string) => { + setAmountInFormValue(amountIn) + if (parseFloat(amountIn) > 0 && sizePrice) { + const amountOut = getAmountOut(amountIn, sizePrice) + setAmountOutFormValue(amountOut.toString()) + } + }, + [getAmountOut, setAmountInFormValue, setAmountOutFormValue, sizePrice], + ) + + const handleRepay = useCallback( + (amountOut: string) => { + setAmountOutFormValue(amountOut) + if (parseFloat(amountOut) > 0 && sizePrice) { + const amountIn = getAmountIn(amountOut, sizePrice) + setAmountInFormValue(amountIn.toString()) + } + }, + [getAmountIn, setAmountInFormValue, setAmountOutFormValue, sizePrice], + ) + const handleAmountInChange = useCallback( (e: NumberFormatValues, info: SourceInfo) => { if (info.source !== 'event') return setAmountInFormValue(e.value) - const price = - orderType === 'trade:stop-market' ? triggerPrice : limitPrice - if (parseFloat(e.value) > 0 && price) { - const amountOut = getAmountOut(e.value, price) + if (parseFloat(e.value) > 0 && sizePrice) { + const amountOut = getAmountOut(e.value, sizePrice) setAmountOutFormValue(amountOut.toString()) } }, - [ - limitPrice, - orderType, - setAmountInFormValue, - setAmountOutFormValue, - triggerPrice, - ], + [getAmountOut, setAmountInFormValue, setAmountOutFormValue, sizePrice], ) const handleAmountOutChange = useCallback( (e: NumberFormatValues, info: SourceInfo) => { if (info.source !== 'event') return setAmountOutFormValue(e.value) - const price = - orderType === 'trade:stop-market' ? triggerPrice : limitPrice - if (parseFloat(e.value) > 0 && price) { - const amountIn = getAmountIn(e.value, price) + if (parseFloat(e.value) > 0 && sizePrice) { + const amountIn = getAmountIn(e.value, sizePrice) setAmountInFormValue(amountIn.toString()) } }, - [ - orderType, - limitPrice, - setAmountInFormValue, - setAmountOutFormValue, - triggerPrice, - ], + [getAmountIn, setAmountInFormValue, setAmountOutFormValue, sizePrice], ) const handleAmountInUi = useCallback( (amountIn: string) => { setAmountInFormValue(amountIn) - const price = - orderType === 'trade:stop-market' ? triggerPrice : limitPrice - if (price) { - const amountOut = getAmountOut(amountIn, price) + if (sizePrice) { + const amountOut = getAmountOut(amountIn, sizePrice) setAmountOutFormValue(amountOut.toString()) } }, - [ - limitPrice, - orderType, - setAmountInFormValue, - setAmountOutFormValue, - triggerPrice, - ], + [getAmountOut, setAmountInFormValue, setAmountOutFormValue, sizePrice], ) const handleLimitPrice = useCallback( @@ -320,12 +323,11 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { ) const handleSwitchTokens = useCallback(() => { - const price = orderType === 'trade:stop-market' ? triggerPrice : limitPrice - if (amountInAsDecimal?.gt(0) && price) { + if (amountInAsDecimal?.gt(0) && sizePrice) { const amountOut = outputBank?.name !== quoteBank?.name - ? amountInAsDecimal.mul(price) - : amountInAsDecimal.div(price) + ? amountInAsDecimal.mul(sizePrice) + : amountInAsDecimal.div(sizePrice) setAmountOutFormValue(amountOut.toString()) } set((s) => { @@ -339,12 +341,11 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { }, [ setAmountInFormValue, amountInAsDecimal, - limitPrice, inputBank, orderType, outputBank, quoteBank, - triggerPrice, + sizePrice, ]) const handlePlaceStopLoss = useCallback(async () => { @@ -427,7 +428,7 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { className="rounded-b-none" handleAmountInChange={handleAmountInChange} setShowTokenSelect={setShowTokenSelect} - setAmountInFormValue={setAmountInFormValue} + handleMax={handleMax} />
{ {swapFormSizeUi === 'slider' ? ( { - set((s) => { - s.swap.amountOut = amountOut - if (!parseFloat(amountOut)) { - s.swap.amountIn = '' - } - }) - }, []) + const setAmountOutFormValue = useCallback( + (amountOut: string, setSwapMode?: boolean) => { + set((s) => { + s.swap.amountOut = amountOut + if (!parseFloat(amountOut)) { + s.swap.amountIn = '' + } + if (setSwapMode) { + s.swap.swapMode = 'ExactOut' + } + }) + }, + [], + ) const handleAmountInChange = useCallback( (e: NumberFormatValues, info: SourceInfo) => { @@ -122,6 +128,20 @@ const MarketSwapForm = ({ [swapMode, setAmountOutFormValue], ) + const handleMax = useCallback( + (amountIn: string) => { + setAmountInFormValue(amountIn, true) + }, + [setAmountInFormValue], + ) + + const handleRepay = useCallback( + (amountOut: string) => { + setAmountOutFormValue(amountOut, true) + }, + [setAmountInFormValue], + ) + /* Once a route is returned from the Jupiter API, use the inAmount or outAmount depending on the swapMode and set those values in state @@ -180,7 +200,7 @@ const MarketSwapForm = ({
From 934e55ec9344ea1868e760039289c3971e351ac9 Mon Sep 17 00:00:00 2001 From: saml33 Date: Wed, 2 Aug 2023 15:32:20 +1000 Subject: [PATCH 14/44] simplify form --- components/swap/LimitSwapForm.tsx | 493 ++++++++++++----------------- components/swap/SellTokenInput.tsx | 13 + components/swap/SwapForm.tsx | 7 +- components/swap/SwapInfoTabs.tsx | 4 +- components/swap/SwapOrders.tsx | 80 ++--- components/swap/SwapPage.tsx | 4 +- public/locales/en/trade.json | 4 +- public/locales/es/trade.json | 4 +- public/locales/ru/trade.json | 4 +- public/locales/zh/trade.json | 4 +- public/locales/zh_tw/trade.json | 4 +- 11 files changed, 264 insertions(+), 357 deletions(-) diff --git a/components/swap/LimitSwapForm.tsx b/components/swap/LimitSwapForm.tsx index 08818f5f..308180df 100644 --- a/components/swap/LimitSwapForm.tsx +++ b/components/swap/LimitSwapForm.tsx @@ -6,7 +6,7 @@ import { Dispatch, SetStateAction, } from 'react' -import { ArrowDownIcon } from '@heroicons/react/20/solid' +import { ArrowDownIcon, ArrowsRightLeftIcon } from '@heroicons/react/20/solid' import NumberFormat, { NumberFormatValues, SourceInfo, @@ -18,7 +18,6 @@ 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' @@ -26,36 +25,32 @@ 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 Button, { IconButton } from '@components/shared/Button' import Loading from '@components/shared/Loading' import TokenLogo from '@components/shared/TokenLogo' import InlineNotification from '@components/shared/InlineNotification' type LimitSwapFormProps = { + showTokenSelect: 'input' | 'output' | undefined setShowTokenSelect: Dispatch> } type LimitSwapForm = { - limitPrice: string | undefined + amountIn: number triggerPrice: string } type FormErrors = Partial> -const ORDER_TYPES = [ - // 'trade:limit', - 'trade:stop-market', - 'trade:stop-limit', -] - const set = mangoStore.getState().set -const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { +const LimitSwapForm = ({ + showTokenSelect, + 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 [flipPrices, setFlipPrices] = useState(false) const [submitting, setSubmitting] = useState(false) const [swapFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider') const [formErrors, setFormErrors] = useState({}) @@ -75,16 +70,6 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { : new Decimal(0) }, [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) => { set((s) => { s.swap.amountIn = amountIn @@ -103,88 +88,57 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { }) }, []) - const setLimitPrice = useCallback((price: string) => { - set((s) => { - s.swap.limitPrice = price - }) - }, []) - - const initialQuotePrice = useMemo(() => { - if (!baseBank || !quoteBank) return - return floorToDecimal( - baseBank.uiPrice / quoteBank.uiPrice, - quoteBank.mintDecimals, - ) - }, [baseBank, quoteBank]) + const [quotePrice, flippedQuotePrice] = useMemo(() => { + if (!inputBank || !outputBank) return [0, 0] + const quote = floorToDecimal( + inputBank.uiPrice / outputBank.uiPrice, + outputBank.mintDecimals, + ).toNumber() + const flipped = floorToDecimal( + outputBank.uiPrice / inputBank.uiPrice, + inputBank.mintDecimals, + ).toNumber() + return [quote, flipped] + }, [inputBank, outputBank]) // set default limit and trigger price useEffect(() => { - if (!initialQuotePrice) return - if (!triggerPrice) { - setTriggerPrice( - initialQuotePrice.mul(0.9).toFixed(quoteBank?.mintDecimals), - ) + if (!quotePrice) return + if (!triggerPrice && !showTokenSelect) { + setTriggerPrice(quotePrice.toFixed(outputBank?.mintDecimals)) } - if (!limitPrice) { - set((s) => { - s.swap.limitPrice = initialQuotePrice - .mul(0.8) - .toFixed(quoteBank?.mintDecimals) - }) - } - }, [initialQuotePrice, limitPrice, quoteBank, triggerPrice]) + }, [quotePrice, outputBank, showTokenSelect, triggerPrice]) - const [limitPriceDifference, triggerPriceDifference] = useMemo(() => { - if (!initialQuotePrice) return [0, 0] - const initialPrice = initialQuotePrice.toNumber() - const limitDifference = limitPrice - ? ((parseFloat(limitPrice) - initialPrice) / initialPrice) * 100 - : 0 + const triggerPriceDifference = useMemo(() => { + if ((!flipPrices && !quotePrice) || (flipPrices && !flippedQuotePrice)) + return 0 + const oraclePrice = !flipPrices ? quotePrice : flippedQuotePrice const triggerDifference = triggerPrice - ? ((parseFloat(triggerPrice) - initialPrice) / initialPrice) * 100 + ? ((parseFloat(triggerPrice) - oraclePrice) / oraclePrice) * 100 : 0 - return [limitDifference, triggerDifference] - }, [initialQuotePrice, limitPrice, triggerPrice]) + return triggerDifference + }, [flippedQuotePrice, quotePrice, triggerPrice]) - // const isFormValid = useCallback( - // (form: LimitSwapForm) => { - // const invalidFields: FormErrors = {} - // setFormErrors({}) - // const triggerPriceNumber = parseFloat(form.triggerPrice) - // const requiredFields: (keyof LimitSwapForm)[] = [ - // 'limitPrice', - // 'triggerPrice', - // ] - // for (const key of requiredFields) { - // const value = form[key] as string - // if (!value) { - // if (orderType === 'trade:stop-market') { - // if (key !== 'limitPrice') { - // invalidFields[key] = t('settings:error-required-field') - // } - // } else { - // invalidFields[key] = t('settings:error-required-field') - // } - // } - // } - // 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], - // ) + const handleTokenSelect = (type: 'input' | 'output') => { + setShowTokenSelect(type) + setTriggerPrice('') + } + + const isFormValid = useCallback((form: LimitSwapForm) => { + const invalidFields: FormErrors = {} + setFormErrors({}) + const requiredFields: (keyof LimitSwapForm)[] = ['amountIn', 'triggerPrice'] + for (const key of requiredFields) { + const value = form[key] as string + if (!value) { + invalidFields[key] = t('settings:error-required-field') + } + } + if (Object.keys(invalidFields).length) { + setFormErrors(invalidFields) + } + return invalidFields + }, []) /* If the use margin setting is toggled, clear the form values @@ -194,115 +148,97 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { 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 const getAmountOut = useCallback( (amountIn: string, price: string) => { - const amountOut = - outputBank?.name === quoteBank?.name - ? floorToDecimal( - parseFloat(amountIn) * parseFloat(price), - outputBank?.mintDecimals || 0, - ) - : floorToDecimal( - parseFloat(amountIn) / parseFloat(price), - outputBank?.mintDecimals || 0, - ) + const amountOut = !flipPrices + ? floorToDecimal( + parseFloat(amountIn) * parseFloat(price), + outputBank?.mintDecimals || 0, + ) + : floorToDecimal( + parseFloat(amountIn) / parseFloat(price), + outputBank?.mintDecimals || 0, + ) return amountOut }, - [outputBank, quoteBank], + [outputBank, flipPrices], ) // get the in amount from the out amount and trigger or limit price const getAmountIn = useCallback( (amountOut: string, price: string) => { - const amountIn = - outputBank?.name === quoteBank?.name - ? floorToDecimal( - parseFloat(amountOut) / parseFloat(price), - inputBank?.mintDecimals || 0, - ) - : floorToDecimal( - parseFloat(amountOut) * parseFloat(price), - inputBank?.mintDecimals || 0, - ) + const amountIn = !flipPrices + ? floorToDecimal( + parseFloat(amountOut) / parseFloat(price), + inputBank?.mintDecimals || 0, + ) + : floorToDecimal( + parseFloat(amountOut) * parseFloat(price), + inputBank?.mintDecimals || 0, + ) return amountIn }, - [inputBank, outputBank, quoteBank], + [inputBank, outputBank, flipPrices], ) const handleMax = useCallback( (amountIn: string) => { setAmountInFormValue(amountIn) - if (parseFloat(amountIn) > 0 && sizePrice) { - const amountOut = getAmountOut(amountIn, sizePrice) + if (parseFloat(amountIn) > 0 && triggerPrice) { + const amountOut = getAmountOut(amountIn, triggerPrice) setAmountOutFormValue(amountOut.toString()) } }, - [getAmountOut, setAmountInFormValue, setAmountOutFormValue, sizePrice], + [getAmountOut, setAmountInFormValue, setAmountOutFormValue, triggerPrice], ) const handleRepay = useCallback( (amountOut: string) => { setAmountOutFormValue(amountOut) - if (parseFloat(amountOut) > 0 && sizePrice) { - const amountIn = getAmountIn(amountOut, sizePrice) + if (parseFloat(amountOut) > 0 && triggerPrice) { + const amountIn = getAmountIn(amountOut, triggerPrice) setAmountInFormValue(amountIn.toString()) } }, - [getAmountIn, setAmountInFormValue, setAmountOutFormValue, sizePrice], + [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 && sizePrice) { - const amountOut = getAmountOut(e.value, sizePrice) + if (parseFloat(e.value) > 0 && triggerPrice) { + const amountOut = getAmountOut(e.value, triggerPrice) setAmountOutFormValue(amountOut.toString()) } }, - [getAmountOut, setAmountInFormValue, setAmountOutFormValue, sizePrice], + [getAmountOut, setAmountInFormValue, setAmountOutFormValue, triggerPrice], ) const handleAmountOutChange = useCallback( (e: NumberFormatValues, info: SourceInfo) => { if (info.source !== 'event') return setAmountOutFormValue(e.value) - if (parseFloat(e.value) > 0 && sizePrice) { - const amountIn = getAmountIn(e.value, sizePrice) + if (parseFloat(e.value) > 0 && triggerPrice) { + const amountIn = getAmountIn(e.value, triggerPrice) setAmountInFormValue(amountIn.toString()) } }, - [getAmountIn, setAmountInFormValue, setAmountOutFormValue, sizePrice], + [getAmountIn, setAmountInFormValue, setAmountOutFormValue, triggerPrice], ) const handleAmountInUi = useCallback( (amountIn: string) => { 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({}) - setLimitPrice(e.value) - if (parseFloat(e.value) > 0 && parseFloat(amountInFormValue) > 0) { - const amountOut = getAmountOut(amountInFormValue, e.value) + if (triggerPrice) { + const amountOut = getAmountOut(amountIn, triggerPrice) setAmountOutFormValue(amountOut.toString()) } }, - [amountInFormValue, setLimitPrice], + [getAmountOut, setAmountInFormValue, setAmountOutFormValue, triggerPrice], ) const handleTriggerPrice = useCallback( @@ -310,49 +246,48 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { if (info.source !== 'event') return setFormErrors({}) setTriggerPrice(e.value) - if ( - parseFloat(e.value) > 0 && - parseFloat(amountInFormValue) > 0 && - orderType === 'trade:stop-market' - ) { + if (parseFloat(e.value) > 0 && parseFloat(amountInFormValue) > 0) { const amountOut = getAmountOut(amountInFormValue, e.value) setAmountOutFormValue(amountOut.toString()) } }, - [amountInFormValue, orderType, setTriggerPrice], + [amountInFormValue, flipPrices, setTriggerPrice], ) const handleSwitchTokens = useCallback(() => { - if (amountInAsDecimal?.gt(0) && sizePrice) { - const amountOut = - outputBank?.name !== quoteBank?.name - ? amountInAsDecimal.mul(sizePrice) - : amountInAsDecimal.div(sizePrice) + if (amountInAsDecimal?.gt(0) && triggerPrice) { + const amountOut = amountInAsDecimal.div(triggerPrice) setAmountOutFormValue(amountOut.toString()) } set((s) => { s.swap.inputBank = outputBank s.swap.outputBank = inputBank - // s.swap.limitPrice = '' }) + + if (flippedQuotePrice) { + setTriggerPrice(flippedQuotePrice.toFixed(inputBank?.mintDecimals)) + } setAnimateSwitchArrow( (prevanimateSwitchArrow) => prevanimateSwitchArrow + 1, ) }, [ setAmountInFormValue, amountInAsDecimal, + flipPrices, + flippedQuotePrice, inputBank, - orderType, outputBank, - quoteBank, - sizePrice, + triggerPrice, ]) const handlePlaceStopLoss = useCallback(async () => { - // const invalidFields = isFormValid({ limitPrice, triggerPrice }) - // if (Object.keys(invalidFields).length) { - // return - // } + const invalidFields = isFormValid({ + amountIn: amountInAsDecimal.toNumber(), + triggerPrice, + }) + if (Object.keys(invalidFields).length) { + return + } try { const client = mangoStore.getState().client const group = mangoStore.getState().group @@ -361,34 +296,25 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { 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') - ) + if (!mangoAccount || !group || !inputBank || !outputBank || !triggerPrice) return setSubmitting(true) - const orderPrice = - orderType === 'trade:limit' - ? parseFloat(limitPrice!) - : parseFloat(triggerPrice) - - const stopLimitPrice = - orderType !== 'trade:stop-market' ? parseFloat(limitPrice!) : 0 + const inputMint = !flipPrices ? inputBank.mint : outputBank.mint + const outputMint = !flipPrices ? outputBank.mint : inputBank.mint + const amountIn = !flipPrices + ? amountInAsDecimal.toNumber() + : parseFloat(amountOutFormValue) try { const tx = await client.tokenConditionalSwapStopLoss( group, mangoAccount, - inputBank.mint, - orderPrice, - outputBank.mint, - stopLimitPrice, - amountInAsDecimal.toNumber(), + inputMint, + parseFloat(triggerPrice), + outputMint, + null, + amountIn, null, null, ) @@ -417,122 +343,98 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => { } finally { setSubmitting(false) } - }, [orderType, limitPrice, triggerPrice, amountInAsDecimal]) + }, [ + flipPrices, + limitPrice, + triggerPrice, + amountInAsDecimal, + amountOutFormValue, + ]) - const limitOrderDisabled = - !connected || !amountInFormValue || !amountOutFormValue + const triggerPriceLabel = useMemo(() => { + 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 ( <> handleTokenSelect('input')} handleMax={handleMax} />
-
-

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

- +
+

+ {triggerPriceLabel}{' '} + + {triggerPriceDifference + ? `(${triggerPriceDifference.toFixed(2)}%)` + : ''} + +

+
+ +
+ +
+
+ handleFlipPrices(!flipPrices)}> + + +
+
+ {formErrors.triggerPrice ? ( +
+ +
+ ) : null}
- {orderType !== 'trade:limit' ? ( -
-

- {t('trade:trigger-price')}{' '} - - {triggerPriceDifference - ? `(${triggerPriceDifference.toFixed(2)}%)` - : ''} - -

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

- {t('trade:limit-price')}{' '} - - {limitPriceDifference - ? `(${limitPriceDifference.toFixed(2)}%)` - : ''} - -

-
- -
- -
-
- {formErrors.limitPrice ? ( -
- -
- ) : null} -
- ) : null}
handleTokenSelect('output')} handleRepay={handleRepay} /> {swapFormSizeUi === 'slider' ? ( @@ -571,7 +473,6 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => {
+ {error ? ( +
+ +
+ ) : null}
) } diff --git a/components/swap/SwapForm.tsx b/components/swap/SwapForm.tsx index f11c098a..c03552fd 100644 --- a/components/swap/SwapForm.tsx +++ b/components/swap/SwapForm.tsx @@ -233,7 +233,7 @@ const SwapForm = () => {
handleSwapOrLimit(v)} />
@@ -254,7 +254,10 @@ const SwapForm = () => { setShowTokenSelect={setShowTokenSelect} /> ) : ( - + )} {ipAllowed ? ( swapOrLimit === 'swap' ? ( diff --git a/components/swap/SwapInfoTabs.tsx b/components/swap/SwapInfoTabs.tsx index 624bcfdd..409e565f 100644 --- a/components/swap/SwapInfoTabs.tsx +++ b/components/swap/SwapInfoTabs.tsx @@ -20,7 +20,7 @@ const SwapInfoTabs = () => { ?.length || 0 return [ ['balances', 0], - ['trade:stop-orders', stopOrdersCount], + ['trade:trigger-orders', stopOrdersCount], ['swap:swap-history', 0], ] }, [mangoAccount]) @@ -41,7 +41,7 @@ const SwapInfoTabs = () => { />
{selectedTab === 'balances' ? : null} - {selectedTab === 'trade:stop-orders' ? : null} + {selectedTab === 'trade:trigger-orders' ? : null} {selectedTab === 'swap:swap-history' ? : null}
) diff --git a/components/swap/SwapOrders.tsx b/components/swap/SwapOrders.tsx index 3b0e80d7..b3d7e73c 100644 --- a/components/swap/SwapOrders.tsx +++ b/components/swap/SwapOrders.tsx @@ -51,23 +51,21 @@ const SwapOrders = () => { sellBank.mintDecimals, ).toNumber() const triggerPrice = order.getPriceLowerLimitUi(group) - const limitPrice = order.getPriceUpperLimitUi(group) const pricePremium = order.getPricePremium() const filled = order.getSoldUi(group) - - const orderType = - limitPrice === 0 ? 'trade:stop-market' : 'trade:stop-limit' + const currentPrice = (sellBank.uiPrice / buyBank.uiPrice).toFixed( + buyBank.mintDecimals, + ) const data = { ...order, buyBank, + currentPrice, sellBank, pair, size, filled, triggerPrice, - limitPrice, - orderType, fee: pricePremium, } formatted.push(data) @@ -136,16 +134,6 @@ const SwapOrders = () => { title={t('swap:pair')} /> - -
- requestSort('orderType')} - sortConfig={sortConfig} - title={t('trade:order-type')} - /> -
-
{
requestSort('triggerPrice')} + sortKey="currentPrice" + sort={() => requestSort('currentPrice')} sortConfig={sortConfig} - title={t('trade:trigger-price')} + title={t('trade:current-price')} />
requestSort('limitPrice')} + sortKey="triggerPrice" + sort={() => requestSort('triggerPrice')} sortConfig={sortConfig} - title={t('trade:limit-price')} + title={t('trade:trigger-price')} />
@@ -203,10 +191,9 @@ const SwapOrders = () => { {tableData.map((data, i) => { const { buyBank, + currentPrice, fee, pair, - orderType, - limitPrice, sellBank, size, filled, @@ -215,9 +202,6 @@ const SwapOrders = () => { return ( {pair} - -

{t(orderType)}

-

{size} @@ -237,36 +221,32 @@ const SwapOrders = () => {

- {triggerPrice ? ( -

- {triggerPrice} - - {' '} - {buyBank.name} - -

- ) : ( -

- )} +

+ {currentPrice} + + {' '} + {buyBank.name} + +

- {limitPrice ? ( -

- {limitPrice} - - {' '} - {buyBank.name} - -

- ) : ( -

- )} +

+ {triggerPrice} + + {' '} + {buyBank.name} + +

{fee.toFixed(2)}%

- handleCancel(data.id)} size="small"> + handleCancel(data.id)} + size="small" + > {cancelId === data.id.toString() ? ( ) : ( diff --git a/components/swap/SwapPage.tsx b/components/swap/SwapPage.tsx index 0e980709..97bedd9c 100644 --- a/components/swap/SwapPage.tsx +++ b/components/swap/SwapPage.tsx @@ -34,10 +34,10 @@ const SwapPage = () => { return ( <>
-
+
-
+
diff --git a/public/locales/en/trade.json b/public/locales/en/trade.json index b2885119..746a6ea0 100644 --- a/public/locales/en/trade.json +++ b/public/locales/en/trade.json @@ -86,7 +86,6 @@ "stop-limit": "Stop Limit", "stop-loss": "Stop-loss", "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": "Play a sound alert for every new trade", "trades": "Trades", "trigger-price": "Trigger Price", + "trigger-order": "Trigger Order", + "trigger-order-rate": "Trigger {{side}} {{symbol}} price", + "trigger-orders": "Trigger Orders", "tweet-position": "Tweet", "unrealized-pnl": "Unrealized PnL", "unsettled": "Unsettled", diff --git a/public/locales/es/trade.json b/public/locales/es/trade.json index b2885119..746a6ea0 100644 --- a/public/locales/es/trade.json +++ b/public/locales/es/trade.json @@ -86,7 +86,6 @@ "stop-limit": "Stop Limit", "stop-loss": "Stop-loss", "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": "Play a sound alert for every new trade", "trades": "Trades", "trigger-price": "Trigger Price", + "trigger-order": "Trigger Order", + "trigger-order-rate": "Trigger {{side}} {{symbol}} price", + "trigger-orders": "Trigger Orders", "tweet-position": "Tweet", "unrealized-pnl": "Unrealized PnL", "unsettled": "Unsettled", diff --git a/public/locales/ru/trade.json b/public/locales/ru/trade.json index b2885119..746a6ea0 100644 --- a/public/locales/ru/trade.json +++ b/public/locales/ru/trade.json @@ -86,7 +86,6 @@ "stop-limit": "Stop Limit", "stop-loss": "Stop-loss", "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": "Play a sound alert for every new trade", "trades": "Trades", "trigger-price": "Trigger Price", + "trigger-order": "Trigger Order", + "trigger-order-rate": "Trigger {{side}} {{symbol}} price", + "trigger-orders": "Trigger Orders", "tweet-position": "Tweet", "unrealized-pnl": "Unrealized PnL", "unsettled": "Unsettled", diff --git a/public/locales/zh/trade.json b/public/locales/zh/trade.json index af10e6ad..73ddf91a 100644 --- a/public/locales/zh/trade.json +++ b/public/locales/zh/trade.json @@ -85,7 +85,6 @@ "stop-limit": "Stop Limit", "stop-loss": "Stop-loss", "stop-market": "Stop Market", - "stop-orders": "Stop Orders", "taker": "吃單者", "tick-size": "波動單位", "taker-fee": "Taker Fee", @@ -102,6 +101,9 @@ "trades": "交易", "tweet-position": "分享至Twitter", "trigger-price": "Trigger Price", + "trigger-order": "Trigger Order", + "trigger-order-rate": "Trigger {{side}} {{symbol}} price", + "trigger-orders": "Trigger Orders", "unsettled": "未結清", "volume-alert": "交易量警報", "volume-alert-desc": "交易量超過警報設定時播放聲音" diff --git a/public/locales/zh_tw/trade.json b/public/locales/zh_tw/trade.json index bee6cf5b..712f479e 100644 --- a/public/locales/zh_tw/trade.json +++ b/public/locales/zh_tw/trade.json @@ -86,7 +86,6 @@ "stop-limit": "Stop Limit", "stop-loss": "Stop-loss", "stop-market": "Stop Market", - "stop-orders": "Stop Orders", "taker": "吃單者", "taker-fee": "吃單者費用", "tick-size": "波動單位", @@ -104,6 +103,9 @@ "trade-sounds-tooltip": "為每筆新交易播放警報聲音", "trades": "交易", "trigger-price": "Trigger Price", + "trigger-order": "Trigger Order", + "trigger-order-rate": "Trigger {{side}} {{symbol}} price", + "trigger-orders": "Trigger Orders", "tweet-position": "分享至Twitter", "unrealized-pnl": "未實現盈虧", "unsettled": "未結清", From 9627ef2d87e64899ef5d1e08281f487d803922b3 Mon Sep 17 00:00:00 2001 From: saml33 Date: Wed, 2 Aug 2023 20:45:06 +1000 Subject: [PATCH 15/44] revert trade form --- components/shared/BalancesTable.tsx | 2 +- components/shared/SuccessParticles.tsx | 2 +- components/trade/AdvancedTradeForm.tsx | 104 ++++--------------- components/trade/MaxSizeButton.tsx | 6 +- components/trade/Orderbook.tsx | 2 +- components/trade/PerpPositions.tsx | 2 +- components/trade/PerpSlider.tsx | 2 +- components/trade/Slippage.tsx | 2 +- components/trade/SpotMarketOrderSwapForm.tsx | 4 +- components/trade/SpotSlider.tsx | 2 +- components/trade/TradeSummary.tsx | 4 +- store/mangoStore.ts | 2 +- types/index.ts | 2 +- 13 files changed, 34 insertions(+), 102 deletions(-) diff --git a/components/shared/BalancesTable.tsx b/components/shared/BalancesTable.tsx index 346778b5..8fd253ab 100644 --- a/components/shared/BalancesTable.tsx +++ b/components/shared/BalancesTable.tsx @@ -358,7 +358,7 @@ const Balance = ({ bank }: { bank: BankWithBalance }) => { if (!group || !selectedMarket) return let price: number - if (tradeForm.tradeType === 'market') { + if (tradeForm.tradeType === 'Market') { const orderbook = mangoStore.getState().selectedMarket.orderbook const side = (balance > 0 && type === 'quote') || (balance < 0 && type === 'base') diff --git a/components/shared/SuccessParticles.tsx b/components/shared/SuccessParticles.tsx index fc87540b..356976d3 100644 --- a/components/shared/SuccessParticles.tsx +++ b/components/shared/SuccessParticles.tsx @@ -33,7 +33,7 @@ const SuccessParticles = () => { return mangoTokens.find((t) => t.address === tokenMint)?.logoURI } } - if (showForTrade && tradeType === 'market') { + if (showForTrade && tradeType === 'Market') { const market = mangoStore.getState().selectedMarket.current const side = mangoStore.getState().tradeForm.side if (market instanceof Serum3Market) { diff --git a/components/trade/AdvancedTradeForm.tsx b/components/trade/AdvancedTradeForm.tsx index 13acacaf..6fcbfca1 100644 --- a/components/trade/AdvancedTradeForm.tsx +++ b/components/trade/AdvancedTradeForm.tsx @@ -48,16 +48,16 @@ import useSelectedMarket from 'hooks/useSelectedMarket' import { floorToDecimal, getDecimalCount } from 'utils/numbers' import LogoWithFallback from '@components/shared/LogoWithFallback' import useIpAddress from 'hooks/useIpAddress' +import ButtonGroup from '@components/forms/ButtonGroup' import TradeSummary from './TradeSummary' import useMangoAccount from 'hooks/useMangoAccount' import MaxSizeButton from './MaxSizeButton' import { INITIAL_SOUND_SETTINGS } from '@components/settings/SoundSettings' import { Howl } from 'howler' import { useWallet } from '@solana/wallet-adapter-react' -import { TradeForm, isMangoError } from 'types' +import { isMangoError } from 'types' import InlineNotification from '@components/shared/InlineNotification' import SpotMarketOrderSwapForm from './SpotMarketOrderSwapForm' -import Select from '@components/forms/Select' const set = mangoStore.getState().set @@ -78,13 +78,6 @@ export const DEFAULT_CHECKBOX_SETTINGS = { margin: false, } -const ORDER_TYPES = [ - 'trade:limit', - 'market', - 'trade:stop-market', - 'trade:stop-limit', -] - const AdvancedTradeForm = () => { const { t } = useTranslation(['common', 'trade']) const { mangoAccount } = useMangoAccount() @@ -109,7 +102,7 @@ const AdvancedTradeForm = () => { serumOrPerpMarket, } = useSelectedMarket() - const setTradeType = useCallback((tradeType: TradeForm['tradeType']) => { + const setTradeType = useCallback((tradeType: 'Limit' | 'Market') => { set((s) => { s.tradeForm.tradeType = tradeType }) @@ -130,22 +123,12 @@ const AdvancedTradeForm = () => { [], ) - const handleTriggerPriceChange = useCallback( - (e: NumberFormatValues, info: SourceInfo) => { - if (info.source !== 'event') return - set((s) => { - s.tradeForm.triggerPrice = e.value - }) - }, - [], - ) - const handleBaseSizeChange = useCallback( (e: NumberFormatValues, info: SourceInfo) => { if (info.source !== 'event') return set((s) => { const price = - s.tradeForm.tradeType === 'market' + s.tradeForm.tradeType === 'Market' ? oraclePrice : Number(s.tradeForm.price) @@ -165,7 +148,7 @@ const AdvancedTradeForm = () => { if (info.source !== 'event') return set((s) => { const price = - s.tradeForm.tradeType === 'market' + s.tradeForm.tradeType === 'Market' ? oraclePrice : Number(s.tradeForm.price) @@ -247,7 +230,7 @@ const AdvancedTradeForm = () => { const { group } = mangoStore.getState() const { tradeType, side, price, baseSize, quoteSize } = tradeForm - const tradePrice = tradeType === 'market' ? oraclePrice : price + const tradePrice = tradeType === 'Market' ? oraclePrice : price if ( !group || @@ -360,7 +343,7 @@ const AdvancedTradeForm = () => { useEffect(() => { const group = mangoStore.getState().group if ( - tradeForm.tradeType === 'market' && + tradeForm.tradeType === 'Market' && oraclePrice && selectedMarket && group @@ -393,7 +376,7 @@ const AdvancedTradeForm = () => { try { const baseSize = Number(tradeForm.baseSize) let price = Number(tradeForm.price) - if (tradeForm.tradeType === 'market') { + if (tradeForm.tradeType === 'Market') { const orderbook = mangoStore.getState().selectedMarket.orderbook price = calculateLimitPriceForMarketOrder( orderbook, @@ -405,7 +388,7 @@ const AdvancedTradeForm = () => { if (selectedMarket instanceof Serum3Market) { const spotOrderType = tradeForm.ioc ? Serum3OrderType.immediateOrCancel - : tradeForm.postOnly && tradeForm.tradeType !== 'market' + : tradeForm.postOnly && tradeForm.tradeType !== 'Market' ? Serum3OrderType.postOnly : Serum3OrderType.limit const tx = await client.serum3PlaceOrder( @@ -434,7 +417,7 @@ const AdvancedTradeForm = () => { }) } else if (selectedMarket instanceof PerpMarket) { const perpOrderType = - tradeForm.tradeType === 'market' + tradeForm.tradeType === 'Market' ? PerpOrderType.market : tradeForm.ioc ? PerpOrderType.immediateOrCancel @@ -496,12 +479,6 @@ const AdvancedTradeForm = () => { connected ? handlePlaceOrder() : connect() } - const tabsToShow = useMemo(() => { - if (!serumOrPerpMarket || serumOrPerpMarket instanceof PerpMarket) { - return ORDER_TYPES.filter((type) => !type.includes('stop')) - } else return ORDER_TYPES - }, [serumOrPerpMarket]) - const disabled = (connected && (!tradeForm.baseSize || !tradeForm.price)) || !serumOrPerpMarket || @@ -524,65 +501,20 @@ const AdvancedTradeForm = () => {

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

- + setTradeType(tab)} + values={['Limit', 'Market']} + />
- {tradeForm.tradeType === 'market' && + {tradeForm.tradeType === 'Market' && selectedMarket instanceof Serum3Market ? ( ) : ( <>
handleSubmit(e)}>
- {tradeForm.tradeType.includes('stop') ? ( - <> -
-

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

-
-
- {quoteLogoURI ? ( -
- -
- ) : ( -
- -
- )} - -
{quoteSymbol}
-
- - ) : null} - {tradeForm.tradeType.includes('limit') ? ( + {tradeForm.tradeType === 'Limit' ? ( <>

@@ -732,7 +664,7 @@ const AdvancedTradeForm = () => { )}

- {tradeForm.tradeType === 'trade:limit' ? ( + {tradeForm.tradeType === 'Limit' ? (
{ if (side === 'buy') { - if (tradeType === 'market' || !price) { + if (tradeType === 'Market' || !price) { const baseSize = floorToDecimal(max / oraclePrice, minOrderDecimals) const quoteSize = floorToDecimal(max, tickDecimals) state.tradeForm.baseSize = baseSize.toFixed() @@ -79,7 +79,7 @@ const MaxSizeButton = ({ } } else { const baseSize = floorToDecimal(max, minOrderDecimals) - if (tradeType === 'market' || !price) { + if (tradeType === 'Market' || !price) { const quoteSize = floorToDecimal( baseSize.mul(oraclePrice), tickDecimals, @@ -106,7 +106,7 @@ const MaxSizeButton = ({ const maxAmount = useMemo(() => { const max = selectedMarket instanceof Serum3Market ? spotMax : perpMax || 0 - const tradePrice = tradeType === 'market' ? oraclePrice : Number(price) + const tradePrice = tradeType === 'Market' ? oraclePrice : Number(price) if (side === 'buy') { return max / tradePrice } else { diff --git a/components/trade/Orderbook.tsx b/components/trade/Orderbook.tsx index 7a56aac1..8f84097b 100644 --- a/components/trade/Orderbook.tsx +++ b/components/trade/Orderbook.tsx @@ -594,7 +594,7 @@ const OrderbookRow = ({ const set = mangoStore.getState().set set((state) => { state.tradeForm.price = formattedPrice.toFixed() - state.tradeForm.tradeType = 'trade:limit' + state.tradeForm.tradeType = 'Limit' if (state.tradeForm.baseSize) { const quoteSize = floorToDecimal( formattedPrice.mul(new Decimal(state.tradeForm.baseSize)), diff --git a/components/trade/PerpPositions.tsx b/components/trade/PerpPositions.tsx index fa216443..e0e89f62 100644 --- a/components/trade/PerpPositions.tsx +++ b/components/trade/PerpPositions.tsx @@ -51,7 +51,7 @@ const PerpPositions = () => { const set = mangoStore.getState().set let price = Number(tradeForm.price) - if (tradeForm.tradeType === 'market') { + if (tradeForm.tradeType === 'Market') { const orderbook = mangoStore.getState().selectedMarket.orderbook price = calculateLimitPriceForMarketOrder( orderbook, diff --git a/components/trade/PerpSlider.tsx b/components/trade/PerpSlider.tsx index 0c0e1196..f3551b58 100644 --- a/components/trade/PerpSlider.tsx +++ b/components/trade/PerpSlider.tsx @@ -55,7 +55,7 @@ const PerpSlider = ({ set((s) => { const price = - s.tradeForm.tradeType === 'market' + s.tradeForm.tradeType === 'Market' ? marketPrice : Number(s.tradeForm.price) diff --git a/components/trade/Slippage.tsx b/components/trade/Slippage.tsx index 82546bb3..9a91cc8c 100644 --- a/components/trade/Slippage.tsx +++ b/components/trade/Slippage.tsx @@ -15,7 +15,7 @@ const Slippage = () => { const slippage = useMemo(() => { try { - if (tradeForm.tradeType === 'market' && markPrice && selectedMarket) { + if (tradeForm.tradeType === 'Market' && markPrice && selectedMarket) { const orderbook = mangoStore.getState().selectedMarket.orderbook return calculateSlippage( orderbook, diff --git a/components/trade/SpotMarketOrderSwapForm.tsx b/components/trade/SpotMarketOrderSwapForm.tsx index a666c554..58e8253c 100644 --- a/components/trade/SpotMarketOrderSwapForm.tsx +++ b/components/trade/SpotMarketOrderSwapForm.tsx @@ -82,7 +82,7 @@ export default function SpotMarketOrderSwapForm() { if (info.source !== 'event') return set((s) => { const price = - s.tradeForm.tradeType === 'market' + s.tradeForm.tradeType === 'Market' ? oraclePrice : Number(s.tradeForm.price) @@ -102,7 +102,7 @@ export default function SpotMarketOrderSwapForm() { if (info.source !== 'event') return set((s) => { const price = - s.tradeForm.tradeType === 'market' + s.tradeForm.tradeType === 'Market' ? oraclePrice : Number(s.tradeForm.price) diff --git a/components/trade/SpotSlider.tsx b/components/trade/SpotSlider.tsx index 9f0b9bb4..5ad5a111 100644 --- a/components/trade/SpotSlider.tsx +++ b/components/trade/SpotSlider.tsx @@ -78,7 +78,7 @@ const SpotSlider = ({ set((s) => { const price = - s.tradeForm.tradeType === 'market' + s.tradeForm.tradeType === 'Market' ? marketPrice : Number(s.tradeForm.price) diff --git a/components/trade/TradeSummary.tsx b/components/trade/TradeSummary.tsx index 2f2621f5..9b93f152 100644 --- a/components/trade/TradeSummary.tsx +++ b/components/trade/TradeSummary.tsx @@ -56,7 +56,7 @@ const TradeSummary = ({ if (!openPosition || !price || !tradeForm.baseSize) return let orderPrice = parseFloat(price) - if (tradeType === 'market') { + if (tradeType === 'Market') { orderPrice = calculateEstPriceForBaseSize( orderbook, parseFloat(tradeForm.baseSize), @@ -283,7 +283,7 @@ const TradeSummary = ({

{t('trade:avg-entry-price')}

- {tradeForm.tradeType === 'market' ? '~' : null} + {tradeForm.tradeType === 'Market' ? '~' : null} Date: Wed, 2 Aug 2023 23:20:09 +1000 Subject: [PATCH 16/44] add order info --- components/swap/LimitSwapForm.tsx | 142 +++++++++++++++++++++--------- public/locales/en/trade.json | 2 +- public/locales/es/trade.json | 2 +- public/locales/ru/trade.json | 2 +- public/locales/zh/trade.json | 2 +- public/locales/zh_tw/trade.json | 2 +- 6 files changed, 105 insertions(+), 47 deletions(-) diff --git a/components/swap/LimitSwapForm.tsx b/components/swap/LimitSwapForm.tsx index 308180df..a60353cd 100644 --- a/components/swap/LimitSwapForm.tsx +++ b/components/swap/LimitSwapForm.tsx @@ -117,7 +117,7 @@ const LimitSwapForm = ({ ? ((parseFloat(triggerPrice) - oraclePrice) / oraclePrice) * 100 : 0 return triggerDifference - }, [flippedQuotePrice, quotePrice, triggerPrice]) + }, [flipPrices, flippedQuotePrice, quotePrice, triggerPrice]) const handleTokenSelect = (type: 'input' | 'output') => { setShowTokenSelect(type) @@ -255,17 +255,18 @@ const LimitSwapForm = ({ ) const handleSwitchTokens = useCallback(() => { - if (amountInAsDecimal?.gt(0) && triggerPrice) { - const amountOut = amountInAsDecimal.div(triggerPrice) - setAmountOutFormValue(amountOut.toString()) - } set((s) => { s.swap.inputBank = outputBank s.swap.outputBank = inputBank }) if (flippedQuotePrice) { - setTriggerPrice(flippedQuotePrice.toFixed(inputBank?.mintDecimals)) + const price = flipPrices ? quotePrice : flippedQuotePrice + setTriggerPrice(price.toFixed(inputBank?.mintDecimals)) + if (amountInAsDecimal?.gt(0)) { + const amountOut = amountInAsDecimal.mul(flippedQuotePrice) + setAmountOutFormValue(amountOut.toString()) + } } setAnimateSwitchArrow( (prevanimateSwitchArrow) => prevanimateSwitchArrow + 1, @@ -351,20 +352,50 @@ const LimitSwapForm = ({ amountOutFormValue, ]) - const triggerPriceLabel = useMemo(() => { - if (!inputBank || !outputBank) return t('trade:trigger-price') + const orderDescription = useMemo(() => { + if ( + !amountInFormValue || + !amountOutFormValue || + !inputBank || + !outputBank || + !triggerPrice + ) + return + const quoteString = !flipPrices + ? `${outputBank.name} per ${inputBank.name}` + : `${inputBank.name} per ${outputBank.name}` if (inputBank.name === 'USDC') { - return t('trade:trigger-order-rate', { - side: t('buy').toLowerCase(), + return t('trade:trigger-order-desc', { + amount: floorToDecimal(amountOutFormValue, outputBank.mintDecimals), symbol: outputBank.name, + triggerPrice: triggerPrice, + priceUnit: quoteString, }) } else { - return t('trade:trigger-order-rate', { - side: t('sell').toLowerCase(), + return t('trade:trigger-order-desc', { + amount: floorToDecimal(amountInFormValue, inputBank.mintDecimals), symbol: inputBank.name, + triggerPrice: triggerPrice, + priceUnit: quoteString, }) } - }, [inputBank, outputBank]) + }, [ + amountInFormValue, + amountOutFormValue, + flipPrices, + inputBank, + outputBank, + triggerPrice, + ]) + + const triggerPriceSuffix = useMemo(() => { + if (!inputBank || !outputBank) return + if (!flipPrices) { + return `${outputBank.name} per ${inputBank.name}` + } else { + return `${inputBank.name} per ${outputBank.name}` + } + }, [flipPrices, inputBank, outputBank]) const handleFlipPrices = useCallback( (flip: boolean) => { @@ -392,36 +423,46 @@ const LimitSwapForm = ({ id="swap-step-two" >

-

- {triggerPriceLabel}{' '} - - {triggerPriceDifference - ? `(${triggerPriceDifference.toFixed(2)}%)` - : ''} - -

-
- -
- +
+

+ {t('trade:trigger-price')}{' '} + + {triggerPriceDifference + ? `(${triggerPriceDifference.toFixed(2)}%)` + : ''} + +

+ handleFlipPrices(!flipPrices)}> + + +
+
+
+ +
+ +
-
- handleFlipPrices(!flipPrices)}> - - +
+ + {triggerPriceSuffix} +
{formErrors.triggerPrice ? ( @@ -470,6 +511,23 @@ const LimitSwapForm = ({ useMargin={useMargin} /> )} + {orderDescription ? ( +
+ + {inputBank?.name === 'USDC' ? ( + {t('buy')} + ) : ( + {t('sell')} + )}{' '} + {orderDescription} + + } + type="info" + /> +
+ ) : null}
-
-

{t('swap:max-slippage')}

- setShowSettings(true)} - > - {slippage}% - -
+ {estSlippage > 0 && swapOrLimit === 'trade:trigger-order' ? ( + <> +
+

+ {t('trade:est-slippage')} +

+ + {estSlippage.toFixed(2)}% + +
+ {outputBank ? ( +
+

+ {t('swap:est-received')} +

+ + {floorToDecimal( + amountOutAsDecimal.div(1 + estSlippage / 100), + outputBank.mintDecimals, + ).toNumber()} + + {' '} + {outputBank?.name} + + +
+ ) : null} + + ) : null} + {swapOrLimit === 'swap' ? ( +
+

+ {t('swap:max-slippage')} +

+ setShowSettings(true)} + > + {slippage}% + +
+ ) : null}
diff --git a/components/swap/SwapOrders.tsx b/components/swap/SwapOrders.tsx index b3d7e73c..b7ec3514 100644 --- a/components/swap/SwapOrders.tsx +++ b/components/swap/SwapOrders.tsx @@ -180,7 +180,7 @@ const SwapOrders = () => { sortKey="fee" sort={() => requestSort('fee')} sortConfig={sortConfig} - title={t('fee')} + title={t('trade:est-slippage')} />
diff --git a/public/locales/en/swap.json b/public/locales/en/swap.json index a087f46d..c76ab89f 100644 --- a/public/locales/en/swap.json +++ b/public/locales/en/swap.json @@ -4,6 +4,7 @@ "disabled": "Disabled", "enabled": "Enabled", "est-liq-price": "Est. Liq Price", + "est-received": "Est. Received", "fees-paid-to": "Fees Paid to {{route}}", "health-impact": "Health Impact", "hide-fees": "Hide Fees", diff --git a/public/locales/es/swap.json b/public/locales/es/swap.json index a087f46d..c76ab89f 100644 --- a/public/locales/es/swap.json +++ b/public/locales/es/swap.json @@ -4,6 +4,7 @@ "disabled": "Disabled", "enabled": "Enabled", "est-liq-price": "Est. Liq Price", + "est-received": "Est. Received", "fees-paid-to": "Fees Paid to {{route}}", "health-impact": "Health Impact", "hide-fees": "Hide Fees", diff --git a/public/locales/ru/swap.json b/public/locales/ru/swap.json index a087f46d..c76ab89f 100644 --- a/public/locales/ru/swap.json +++ b/public/locales/ru/swap.json @@ -4,6 +4,7 @@ "disabled": "Disabled", "enabled": "Enabled", "est-liq-price": "Est. Liq Price", + "est-received": "Est. Received", "fees-paid-to": "Fees Paid to {{route}}", "health-impact": "Health Impact", "hide-fees": "Hide Fees", diff --git a/public/locales/zh/swap.json b/public/locales/zh/swap.json index 5eb69360..5fb02523 100644 --- a/public/locales/zh/swap.json +++ b/public/locales/zh/swap.json @@ -4,6 +4,7 @@ "disabled": "Disabled", "enabled": "Enabled", "est-liq-price": "预计清算价格", + "est-received": "Est. Received", "fees-paid-to": "缴给{{route}}的费用", "health-impact": "健康影响", "hide-fees": "隐藏费用", diff --git a/public/locales/zh_tw/swap.json b/public/locales/zh_tw/swap.json index 8b22e38a..4eb58b20 100644 --- a/public/locales/zh_tw/swap.json +++ b/public/locales/zh_tw/swap.json @@ -4,6 +4,7 @@ "disabled": "關閉", "enabled": "開啟", "est-liq-price": "預計清算價格", + "est-received": "Est. Received", "fees-paid-to": "繳給{{route}}的費用", "health-impact": "健康影響", "hide-fees": "隱藏費用", From ce1939b63cf7b7fa19191cc90a9e841718e8ccfd Mon Sep 17 00:00:00 2001 From: saml33 Date: Thu, 3 Aug 2023 20:34:57 +1000 Subject: [PATCH 18/44] save chart direction --- apis/coingecko.ts | 21 ++-- components/swap/LimitSwapForm.tsx | 154 +++++++++++++++++++---------- components/swap/SwapOrders.tsx | 2 + components/swap/SwapTokenChart.tsx | 112 ++++++++++++++++----- types/index.ts | 5 + utils/constants.ts | 6 +- 6 files changed, 204 insertions(+), 96 deletions(-) diff --git a/apis/coingecko.ts b/apis/coingecko.ts index d6e3b96c..d9000a29 100644 --- a/apis/coingecko.ts +++ b/apis/coingecko.ts @@ -39,21 +39,12 @@ export const fetchChartData = async ( (outputTokenCandle) => outputTokenCandle[0] === inputTokenCandle[0], ) if (outputTokenCandle) { - if (['usd-coin', 'tether'].includes(quoteTokenId)) { - parsedData.push({ - time: inputTokenCandle[0], - price: inputTokenCandle[4] / outputTokenCandle[4], - inputTokenPrice: inputTokenCandle[4], - outputTokenPrice: outputTokenCandle[4], - }) - } else { - parsedData.push({ - time: inputTokenCandle[0], - price: outputTokenCandle[4] / inputTokenCandle[4], - inputTokenPrice: inputTokenCandle[4], - outputTokenPrice: outputTokenCandle[4], - }) - } + parsedData.push({ + time: inputTokenCandle[0], + price: outputTokenCandle[4] / inputTokenCandle[4], + inputTokenPrice: inputTokenCandle[4], + outputTokenPrice: outputTokenCandle[4], + }) } } return parsedData diff --git a/components/swap/LimitSwapForm.tsx b/components/swap/LimitSwapForm.tsx index a60353cd..345a4eb7 100644 --- a/components/swap/LimitSwapForm.tsx +++ b/components/swap/LimitSwapForm.tsx @@ -14,7 +14,10 @@ import NumberFormat, { import Decimal from 'decimal.js' import mangoStore from '@store/mangoStore' import { useTranslation } from 'next-i18next' -import { SIZE_INPUT_UI_KEY } from '../../utils/constants' +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' @@ -24,11 +27,12 @@ import SellTokenInput from './SellTokenInput' import BuyTokenInput from './BuyTokenInput' import { notify } from 'utils/notifications' import * as sentry from '@sentry/nextjs' -import { isMangoError } from 'types' +import { SwapChartSettings, isMangoError } from 'types' import Button, { IconButton } from '@components/shared/Button' import Loading from '@components/shared/Loading' import TokenLogo from '@components/shared/TokenLogo' import InlineNotification from '@components/shared/InlineNotification' +import { handleFlipPrices } from './SwapTokenChart' type LimitSwapFormProps = { showTokenSelect: 'input' | 'output' | undefined @@ -50,10 +54,13 @@ const LimitSwapForm = ({ const { t } = useTranslation(['common', 'swap', 'trade']) const [animateSwitchArrow, setAnimateSwitchArrow] = useState(0) const [triggerPrice, setTriggerPrice] = useState('') - const [flipPrices, setFlipPrices] = useState(false) 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 { margin: useMargin, @@ -70,6 +77,18 @@ const LimitSwapForm = ({ : new Decimal(0) }, [amountInFormValue]) + const flipPrices = useMemo(() => { + if (!swapChartSettings.length || !inputBank || !outputBank) return false + const pairSettings = swapChartSettings.find( + (chart: SwapChartSettings) => + chart.pair.includes(inputBank.name) && + chart.pair.includes(outputBank.name), + ) + if (pairSettings) { + return pairSettings.flipPrices + } else return false + }, [swapChartSettings, inputBank, outputBank]) + const setAmountInFormValue = useCallback((amountIn: string) => { set((s) => { s.swap.amountIn = amountIn @@ -88,36 +107,35 @@ const LimitSwapForm = ({ }) }, []) - const [quotePrice, flippedQuotePrice] = useMemo(() => { - if (!inputBank || !outputBank) return [0, 0] - const quote = floorToDecimal( - inputBank.uiPrice / outputBank.uiPrice, - outputBank.mintDecimals, - ).toNumber() - const flipped = floorToDecimal( - outputBank.uiPrice / inputBank.uiPrice, - inputBank.mintDecimals, - ).toNumber() - return [quote, flipped] - }, [inputBank, outputBank]) + 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 limit and trigger price useEffect(() => { if (!quotePrice) return if (!triggerPrice && !showTokenSelect) { - setTriggerPrice(quotePrice.toFixed(outputBank?.mintDecimals)) + setTriggerPrice(quotePrice.toFixed(inputBank?.mintDecimals)) } - }, [quotePrice, outputBank, showTokenSelect, triggerPrice]) + }, [inputBank, quotePrice, showTokenSelect, triggerPrice]) const triggerPriceDifference = useMemo(() => { - if ((!flipPrices && !quotePrice) || (flipPrices && !flippedQuotePrice)) - return 0 - const oraclePrice = !flipPrices ? quotePrice : flippedQuotePrice + if (!quotePrice) return 0 const triggerDifference = triggerPrice - ? ((parseFloat(triggerPrice) - oraclePrice) / oraclePrice) * 100 + ? ((parseFloat(triggerPrice) - quotePrice) / quotePrice) * 100 : 0 return triggerDifference - }, [flipPrices, flippedQuotePrice, quotePrice, triggerPrice]) + }, [flipPrices, quotePrice, triggerPrice]) const handleTokenSelect = (type: 'input' | 'output') => { setShowTokenSelect(type) @@ -153,11 +171,11 @@ const LimitSwapForm = ({ (amountIn: string, price: string) => { const amountOut = !flipPrices ? floorToDecimal( - parseFloat(amountIn) * parseFloat(price), + parseFloat(amountIn) / parseFloat(price), outputBank?.mintDecimals || 0, ) : floorToDecimal( - parseFloat(amountIn) / parseFloat(price), + parseFloat(amountIn) * parseFloat(price), outputBank?.mintDecimals || 0, ) return amountOut @@ -170,11 +188,11 @@ const LimitSwapForm = ({ (amountOut: string, price: string) => { const amountIn = !flipPrices ? floorToDecimal( - parseFloat(amountOut) / parseFloat(price), + parseFloat(amountOut) * parseFloat(price), inputBank?.mintDecimals || 0, ) : floorToDecimal( - parseFloat(amountOut) * parseFloat(price), + parseFloat(amountOut) / parseFloat(price), inputBank?.mintDecimals || 0, ) return amountIn @@ -255,18 +273,26 @@ const LimitSwapForm = ({ ) const handleSwitchTokens = useCallback(() => { + if (!inputBank || !outputBank) return set((s) => { s.swap.inputBank = outputBank s.swap.outputBank = inputBank }) - if (flippedQuotePrice) { - const price = flipPrices ? quotePrice : flippedQuotePrice - setTriggerPrice(price.toFixed(inputBank?.mintDecimals)) - if (amountInAsDecimal?.gt(0)) { - const amountOut = amountInAsDecimal.mul(flippedQuotePrice) - setAmountOutFormValue(amountOut.toString()) - } + const price = !flipPrices + ? floorToDecimal( + inputBank.uiPrice / outputBank.uiPrice, + outputBank.mintDecimals, + ).toString() + : floorToDecimal( + outputBank.uiPrice / inputBank.uiPrice, + inputBank.mintDecimals, + ).toString() + setTriggerPrice(price) + + if (amountInAsDecimal?.gt(0)) { + const amountOut = getAmountOut(amountInAsDecimal.toString(), price) + setAmountOutFormValue(amountOut.toString()) } setAnimateSwitchArrow( (prevanimateSwitchArrow) => prevanimateSwitchArrow + 1, @@ -275,7 +301,6 @@ const LimitSwapForm = ({ setAmountInFormValue, amountInAsDecimal, flipPrices, - flippedQuotePrice, inputBank, outputBank, triggerPrice, @@ -301,11 +326,9 @@ const LimitSwapForm = ({ return setSubmitting(true) - const inputMint = !flipPrices ? inputBank.mint : outputBank.mint - const outputMint = !flipPrices ? outputBank.mint : inputBank.mint - const amountIn = !flipPrices - ? amountInAsDecimal.toNumber() - : parseFloat(amountOutFormValue) + const inputMint = inputBank.mint + const outputMint = outputBank.mint + const amountIn = amountInAsDecimal.toNumber() try { const tx = await client.tokenConditionalSwapStopLoss( @@ -362,8 +385,8 @@ const LimitSwapForm = ({ ) return const quoteString = !flipPrices - ? `${outputBank.name} per ${inputBank.name}` - : `${inputBank.name} per ${outputBank.name}` + ? `${inputBank.name} per ${outputBank.name}` + : `${outputBank.name} per ${inputBank.name}` if (inputBank.name === 'USDC') { return t('trade:trigger-order-desc', { amount: floorToDecimal(amountOutFormValue, outputBank.mintDecimals), @@ -391,22 +414,41 @@ const LimitSwapForm = ({ const triggerPriceSuffix = useMemo(() => { if (!inputBank || !outputBank) return if (!flipPrices) { - return `${outputBank.name} per ${inputBank.name}` - } else { return `${inputBank.name} per ${outputBank.name}` + } else { + return `${outputBank.name} per ${inputBank.name}` } }, [flipPrices, inputBank, outputBank]) - const handleFlipPrices = useCallback( + const toggleFlipPrices = useCallback( (flip: boolean) => { - setFlipPrices(flip) - if (flip) { - setTriggerPrice(flippedQuotePrice.toFixed(inputBank?.mintDecimals)) - } else { - setTriggerPrice(quotePrice.toFixed(outputBank?.mintDecimals)) - } + if (!inputBank || !outputBank) return + handleFlipPrices( + flip, + flipPrices, + inputBank.name, + outputBank.name, + swapChartSettings, + setSwapChartSettings, + ) + const price = !flipPrices + ? floorToDecimal( + inputBank.uiPrice / outputBank.uiPrice, + outputBank.mintDecimals, + ).toString() + : floorToDecimal( + outputBank.uiPrice / inputBank.uiPrice, + inputBank.mintDecimals, + ).toString() + setTriggerPrice(price) }, - [flippedQuotePrice, inputBank, outputBank, quotePrice], + [ + flipPrices, + inputBank, + outputBank, + swapChartSettings, + setSwapChartSettings, + ], ) return ( @@ -432,7 +474,7 @@ const LimitSwapForm = ({ : ''}

- handleFlipPrices(!flipPrices)}> + toggleFlipPrices(!flipPrices)}>
@@ -443,7 +485,11 @@ const LimitSwapForm = ({ thousandSeparator="," allowNegative={false} isNumericString={true} - decimalScale={outputBank?.mintDecimals || 6} + decimalScale={ + !flipPrices + ? inputBank?.mintDecimals + : outputBank?.mintDecimals || 6 + } name="triggerPrice" id="triggerPrice" className="h-10 w-full rounded-l-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" @@ -454,7 +500,7 @@ const LimitSwapForm = ({ />
diff --git a/components/swap/SwapOrders.tsx b/components/swap/SwapOrders.tsx index b7ec3514..7fc142af 100644 --- a/components/swap/SwapOrders.tsx +++ b/components/swap/SwapOrders.tsx @@ -122,6 +122,8 @@ const SwapOrders = () => { } } + console.log(orders) + return orders.length ? ( diff --git a/components/swap/SwapTokenChart.tsx b/components/swap/SwapTokenChart.tsx index b9340360..8d6b7a56 100644 --- a/components/swap/SwapTokenChart.tsx +++ b/components/swap/SwapTokenChart.tsx @@ -32,7 +32,10 @@ import { ChartDataItem, fetchChartData } from 'apis/coingecko' import mangoStore from '@store/mangoStore' import useJupiterSwapData from './useJupiterSwapData' import useLocalStorageState from 'hooks/useLocalStorageState' -import { ANIMATION_SETTINGS_KEY } from 'utils/constants' +import { + ANIMATION_SETTINGS_KEY, + SWAP_CHART_SETTINGS_KEY, +} from 'utils/constants' import { INITIAL_ANIMATION_SETTINGS } from '@components/settings/AnimationSettings' import { useTranslation } from 'next-i18next' import { @@ -46,11 +49,35 @@ import { CategoricalChartFunc } from 'recharts/types/chart/generateCategoricalCh import { interpolateNumber } from 'd3-interpolate' import { IconButton } from '@components/shared/Button' import Tooltip from '@components/shared/Tooltip' -import { SwapHistoryItem } from 'types' +import { SwapChartSettings, SwapHistoryItem } from 'types' import useThemeWrapper from 'hooks/useThemeWrapper' dayjs.extend(relativeTime) +export const handleFlipPrices = ( + flip: boolean, + flipPrices: boolean, + inputToken: string | undefined, + outputToken: string | undefined, + swapChartSettings: SwapChartSettings[], + setSwapChartSettings: (settings: SwapChartSettings[]) => void, +) => { + if (!inputToken || !outputToken) return + if (!flipPrices && flip) { + setSwapChartSettings([ + ...swapChartSettings, + { pair: `${inputToken}/${outputToken}`, flipPrices: true }, + ]) + } else { + setSwapChartSettings( + swapChartSettings.filter( + (chart: SwapChartSettings) => + !chart.pair.includes(inputToken) && !chart.pair.includes(outputToken), + ), + ) + } +} + const CustomizedLabel = ({ chartData, x, @@ -182,12 +209,16 @@ const SwapTokenChart = () => { const [quoteTokenId, setQuoteTokenId] = useState(outputCoingeckoId) const [mouseData, setMouseData] = useState() const [daysToShow, setDaysToShow] = useState('1') - const [flipPrices, setFlipPrices] = useState(false) + // const [flipPrices, setFlipPrices] = useState(false) const { theme } = useThemeWrapper() const [animationSettings] = useLocalStorageState( ANIMATION_SETTINGS_KEY, INITIAL_ANIMATION_SETTINGS, ) + const [swapChartSettings, setSwapChartSettings] = useLocalStorageState( + SWAP_CHART_SETTINGS_KEY, + [], + ) const swapHistory = mangoStore((s) => s.mangoAccount.swapHistory.data) const loadSwapHistory = mangoStore((s) => s.mangoAccount.swapHistory.loading) const [showSwaps, setShowSwaps] = useState(true) @@ -197,6 +228,47 @@ const SwapTokenChart = () => { string | number | undefined >(undefined) + const flipPrices = useMemo(() => { + if (!swapChartSettings.length || !inputBank || !outputBank) return false + const pairSettings = swapChartSettings.find( + (chart: SwapChartSettings) => + chart.pair.includes(inputBank.name) && + chart.pair.includes(outputBank.name), + ) + if (pairSettings) { + return pairSettings.flipPrices + } else return false + }, [swapChartSettings, inputBank, outputBank]) + + const swapMarketName = useMemo(() => { + if (!inputBank || !outputBank) return '' + const inputSymbol = formatTokenSymbol(inputBank.name) + const outputSymbol = formatTokenSymbol(outputBank.name) + return !flipPrices + ? `${outputSymbol}/${inputSymbol}` + : `${inputSymbol}/${outputSymbol}` + }, [flipPrices, inputBank, inputCoingeckoId, outputBank]) + + // const handleFlipPrices = useCallback( + // (flip: boolean) => { + // if (!flipPrices && flip) { + // setSwapChartSettings([ + // ...swapChartSettings, + // { pair: swapMarketName, flipPrices: true }, + // ]) + // } else { + // setSwapChartSettings( + // swapChartSettings.filter( + // (chart: SwapChartSettings) => + // !chart.pair.includes(inputBank!.name) && + // !chart.pair.includes(outputBank!.name), + // ), + // ) + // } + // }, + // [flipPrices, inputBank, outputBank, swapChartSettings, swapMarketName], + // ) + const handleSwapMouseEnter = useCallback( ( swap: SwapHistoryItem | undefined, @@ -216,19 +288,6 @@ const SwapTokenChart = () => { setSwapTooltipData(null) }, [setSwapTooltipData]) - const swapMarketName = useMemo(() => { - if (!inputBank || !outputBank) return '' - const inputSymbol = formatTokenSymbol(inputBank.name) - const outputSymbol = formatTokenSymbol(outputBank.name) - return ['usd-coin', 'tether'].includes(inputCoingeckoId || '') - ? !flipPrices - ? `${outputSymbol}/${inputSymbol}` - : `${inputSymbol}/${outputSymbol}` - : !flipPrices - ? `${inputSymbol}/${outputSymbol}` - : `${outputSymbol}/${inputSymbol}` - }, [flipPrices, inputBank, inputCoingeckoId, outputBank]) - const renderTooltipContent = useCallback( (swap: SwapHistoryItem) => { const { @@ -429,14 +488,8 @@ const SwapTokenChart = () => { useEffect(() => { if (!inputCoingeckoId || !outputCoingeckoId) return - - if (['usd-coin', 'tether'].includes(outputCoingeckoId)) { - setBaseTokenId(inputCoingeckoId) - setQuoteTokenId(outputCoingeckoId) - } else { - setBaseTokenId(outputCoingeckoId) - setQuoteTokenId(inputCoingeckoId) - } + setBaseTokenId(inputCoingeckoId) + setQuoteTokenId(outputCoingeckoId) }, [inputCoingeckoId, outputCoingeckoId]) const calculateChartChange = useCallback(() => { @@ -485,7 +538,16 @@ const SwapTokenChart = () => {

{swapMarketName}

setFlipPrices(!flipPrices)} + onClick={() => + handleFlipPrices( + !flipPrices, + flipPrices, + inputBank.name, + outputBank.name, + swapChartSettings, + setSwapChartSettings, + ) + } hideBg > diff --git a/types/index.ts b/types/index.ts index ff55d4a9..ae6ed324 100644 --- a/types/index.ts +++ b/types/index.ts @@ -466,3 +466,8 @@ export interface ContributionDetails { perpMarketContributions: PerpMarketContribution[] spotUi: number } + +export type SwapChartSettings = { + flipPrices: boolean + pair: string +} diff --git a/utils/constants.ts b/utils/constants.ts index f2eedf7a..286dbf9c 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -6,10 +6,10 @@ export const CLIENT_TX_TIMEOUT = 90000 export const SECONDS = 1000 -export const INPUT_TOKEN_DEFAULT = 'USDC' +export const INPUT_TOKEN_DEFAULT = 'SOL' export const MANGO_MINT = 'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac' export const USDC_MINT = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' -export const OUTPUT_TOKEN_DEFAULT = 'SOL' +export const OUTPUT_TOKEN_DEFAULT = 'MNGO' export const JUPITER_V4_PROGRAM_ID = 'JUP4Fb2cqiRUcaTHdrPC8h2gNsA2ETXiPDD33WcGuJB' @@ -57,6 +57,8 @@ export const SWAP_MARGIN_KEY = 'swapMargin-0.1' export const SHOW_SWAP_INTRO_MODAL = 'showSwapModal-0.1' +export const SWAP_CHART_SETTINGS_KEY = 'swapChartSettings-0.1' + export const ACCEPT_TERMS_KEY = 'termsOfUseAccepted-0.1' export const TRADE_LAYOUT_KEY = 'tradeLayoutKey-0.1' From a2f2c0fe031c03b0793459ead0d589e79adc1ad2 Mon Sep 17 00:00:00 2001 From: saml33 Date: Thu, 3 Aug 2023 23:44:23 +1000 Subject: [PATCH 19/44] add order types back --- components/swap/LimitSwapForm.tsx | 122 ++++++++++++++++++++++++----- components/swap/SellTokenInput.tsx | 4 +- components/swap/SwapForm.tsx | 59 +++++++------- components/swap/SwapOrders.tsx | 2 - 4 files changed, 134 insertions(+), 53 deletions(-) diff --git a/components/swap/LimitSwapForm.tsx b/components/swap/LimitSwapForm.tsx index 345a4eb7..af16db1c 100644 --- a/components/swap/LimitSwapForm.tsx +++ b/components/swap/LimitSwapForm.tsx @@ -33,6 +33,7 @@ import Loading from '@components/shared/Loading' import TokenLogo from '@components/shared/TokenLogo' import InlineNotification from '@components/shared/InlineNotification' import { handleFlipPrices } from './SwapTokenChart' +import Select from '@components/forms/Select' type LimitSwapFormProps = { showTokenSelect: 'input' | 'output' | undefined @@ -45,6 +46,18 @@ type LimitSwapForm = { } type FormErrors = Partial> +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 LimitSwapForm = ({ @@ -54,6 +67,8 @@ const LimitSwapForm = ({ const { t } = useTranslation(['common', 'swap', 'trade']) const [animateSwitchArrow, setAnimateSwitchArrow] = useState(0) const [triggerPrice, setTriggerPrice] = useState('') + const [orderType, setOrderType] = useState(ORDER_TYPES[0]) + const [orderTypeMultiplier, setOrderTypeMultiplier] = useState(0.9) const [submitting, setSubmitting] = useState(false) const [swapFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider') const [formErrors, setFormErrors] = useState({}) @@ -121,11 +136,11 @@ const LimitSwapForm = ({ return quote }, [flipPrices, inputBank, outputBank]) - // set default limit and trigger price + // set default trigger price useEffect(() => { if (!quotePrice) return if (!triggerPrice && !showTokenSelect) { - setTriggerPrice(quotePrice.toFixed(inputBank?.mintDecimals)) + setTriggerPrice((quotePrice * 0.9).toFixed(inputBank?.mintDecimals)) } }, [inputBank, quotePrice, showTokenSelect, triggerPrice]) @@ -281,11 +296,11 @@ const LimitSwapForm = ({ const price = !flipPrices ? floorToDecimal( - inputBank.uiPrice / outputBank.uiPrice, + (inputBank.uiPrice / outputBank.uiPrice) * orderTypeMultiplier, outputBank.mintDecimals, ).toString() : floorToDecimal( - outputBank.uiPrice / inputBank.uiPrice, + (outputBank.uiPrice / inputBank.uiPrice) * orderTypeMultiplier, inputBank.mintDecimals, ).toString() setTriggerPrice(price) @@ -302,6 +317,7 @@ const LimitSwapForm = ({ amountInAsDecimal, flipPrices, inputBank, + orderTypeMultiplier, outputBank, triggerPrice, ]) @@ -433,11 +449,11 @@ const LimitSwapForm = ({ ) const price = !flipPrices ? floorToDecimal( - inputBank.uiPrice / outputBank.uiPrice, + (inputBank.uiPrice / outputBank.uiPrice) * orderTypeMultiplier, outputBank.mintDecimals, ).toString() : floorToDecimal( - outputBank.uiPrice / inputBank.uiPrice, + (outputBank.uiPrice / inputBank.uiPrice) * orderTypeMultiplier, inputBank.mintDecimals, ).toString() setTriggerPrice(price) @@ -445,12 +461,53 @@ const LimitSwapForm = ({ [ flipPrices, inputBank, + orderTypeMultiplier, outputBank, swapChartSettings, setSwapChartSettings, ], ) + const hasBorrowToRepay = useMemo(() => { + const mangoAccount = mangoStore.getState().mangoAccount.current + if (orderType !== OrderTypes.REPAY_BORROW || !outputBank || !mangoAccount) + return + const borrow = mangoAccount.getTokenBorrowsUi(outputBank) + return borrow + }, [orderType, outputBank]) + + const sellTokenBalance = useMemo(() => { + const mangoAccount = mangoStore.getState().mangoAccount.current + if (!inputBank || !mangoAccount) return 0 + const balance = mangoAccount.getTokenBalanceUi(inputBank) + return balance + }, [inputBank]) + + const handleOrderTypeChange = useCallback( + (type: string) => { + const newType = type as OrderTypes + setOrderType(newType) + const triggerMultiplier = + newType === OrderTypes.STOP_LOSS + ? 0.9 + : newType === OrderTypes.TAKE_PROFIT + ? 1.1 + : 1 + setOrderTypeMultiplier(triggerMultiplier) + const trigger = (quotePrice * triggerMultiplier).toString() + setTriggerPrice(trigger) + }, + [quotePrice, 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')}{' '} - - {triggerPriceDifference - ? `(${triggerPriceDifference.toFixed(2)}%)` - : ''} - -

+

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

toggleFlipPrices(!flipPrices)}> @@ -492,7 +558,7 @@ const LimitSwapForm = ({ } name="triggerPrice" id="triggerPrice" - className="h-10 w-full rounded-l-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" + 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} @@ -505,11 +571,19 @@ const LimitSwapForm = ({ />
-
- - {triggerPriceSuffix} - -
+
+
+ = 0 ? 'text-th-up' : 'text-th-down' + } + > + {triggerPriceDifference + ? triggerPriceDifference.toFixed(2) + : '0.00'} + % + + {triggerPriceSuffix}
{formErrors.triggerPrice ? (
@@ -574,8 +648,14 @@ const LimitSwapForm = ({ />
) : null} + {orderType === 'repay-borrow' && !hasBorrowToRepay ? ( +
+ +
+ ) : null}
diff --git a/components/swap/SwapOrders.tsx b/components/swap/SwapOrders.tsx index 7fc142af..b7ec3514 100644 --- a/components/swap/SwapOrders.tsx +++ b/components/swap/SwapOrders.tsx @@ -122,8 +122,6 @@ const SwapOrders = () => { } } - console.log(orders) - return orders.length ? (
From 78da92f4534155bdc6e3ac34aa675186654fe6bc Mon Sep 17 00:00:00 2001 From: saml33 Date: Fri, 4 Aug 2023 12:06:09 +1000 Subject: [PATCH 20/44] various fixes --- components/shared/TokenVaultWarnings.tsx | 6 +- components/swap/LimitSwapForm.tsx | 103 ++++++++--- components/swap/MarketSwapForm.tsx | 157 ++++++++++++++-- components/swap/SwapForm.tsx | 217 ++--------------------- components/swap/SwapTokenChart.tsx | 41 +++-- public/locales/en/trade.json | 3 +- public/locales/es/trade.json | 3 +- public/locales/ru/trade.json | 3 +- public/locales/zh/trade.json | 3 +- public/locales/zh_tw/trade.json | 3 +- 10 files changed, 269 insertions(+), 270 deletions(-) diff --git a/components/shared/TokenVaultWarnings.tsx b/components/shared/TokenVaultWarnings.tsx index 77793339..d0f3ea2a 100644 --- a/components/shared/TokenVaultWarnings.tsx +++ b/components/shared/TokenVaultWarnings.tsx @@ -23,7 +23,11 @@ const TokenVaultWarnings = ({ const [maxWithdraw, maxBorrow] = useMemo(() => { if (!mangoAccount || !group) return [0, 0] - const maxWithdraw = getMaxWithdrawForBank(group, bank, mangoAccount) + const maxWithdraw = getMaxWithdrawForBank( + group, + bank, + mangoAccount, + ).toNumber() const maxBorrow = mangoAccount.getMaxWithdrawWithBorrowForTokenUi( group, bank.mint, diff --git a/components/swap/LimitSwapForm.tsx b/components/swap/LimitSwapForm.tsx index af16db1c..9acc2747 100644 --- a/components/swap/LimitSwapForm.tsx +++ b/components/swap/LimitSwapForm.tsx @@ -5,6 +5,7 @@ import { useMemo, Dispatch, SetStateAction, + useLayoutEffect, } from 'react' import { ArrowDownIcon, ArrowsRightLeftIcon } from '@heroicons/react/20/solid' import NumberFormat, { @@ -34,6 +35,7 @@ import TokenLogo from '@components/shared/TokenLogo' import InlineNotification from '@components/shared/InlineNotification' import { handleFlipPrices } from './SwapTokenChart' import Select from '@components/forms/Select' +import useIpAddress from 'hooks/useIpAddress' type LimitSwapFormProps = { showTokenSelect: 'input' | 'output' | undefined @@ -65,10 +67,11 @@ const LimitSwapForm = ({ setShowTokenSelect, }: LimitSwapFormProps) => { const { t } = useTranslation(['common', 'swap', 'trade']) + const { ipAllowed, ipCountry } = useIpAddress() const [animateSwitchArrow, setAnimateSwitchArrow] = useState(0) const [triggerPrice, setTriggerPrice] = useState('') const [orderType, setOrderType] = useState(ORDER_TYPES[0]) - const [orderTypeMultiplier, setOrderTypeMultiplier] = useState(0.9) + const [orderTypeMultiplier, setOrderTypeMultiplier] = useState(1.1) const [submitting, setSubmitting] = useState(false) const [swapFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider') const [formErrors, setFormErrors] = useState({}) @@ -140,10 +143,18 @@ const LimitSwapForm = ({ useEffect(() => { if (!quotePrice) return if (!triggerPrice && !showTokenSelect) { - setTriggerPrice((quotePrice * 0.9).toFixed(inputBank?.mintDecimals)) + setTriggerPrice((quotePrice * 1.1).toFixed(inputBank?.mintDecimals)) } }, [inputBank, quotePrice, showTokenSelect, triggerPrice]) + // flip trigger price when chart direction is flipped + useLayoutEffect(() => { + if (!quotePrice) return + setTriggerPrice( + (quotePrice * orderTypeMultiplier).toFixed(inputBank?.mintDecimals), + ) + }, [flipPrices, orderTypeMultiplier]) + const triggerPriceDifference = useMemo(() => { if (!quotePrice) return 0 const triggerDifference = triggerPrice @@ -173,9 +184,7 @@ const LimitSwapForm = ({ return invalidFields }, []) - /* - If the use margin setting is toggled, clear the form values - */ + // If the use margin setting is toggled, clear the form values useEffect(() => { setAmountInFormValue('') setAmountOutFormValue('') @@ -400,22 +409,38 @@ const LimitSwapForm = ({ !triggerPrice ) return + const quoteString = !flipPrices ? `${inputBank.name} per ${outputBank.name}` : `${outputBank.name} per ${inputBank.name}` - if (inputBank.name === 'USDC') { + + const orderTypeString = + orderType === OrderTypes.STOP_LOSS + ? t('trade:falls-to') + : t('trade:rises-to') + + if (orderType === OrderTypes.REPAY_BORROW) { + return t('trade:repay-borrow-order-desc', { + amount: floorToDecimal(amountOutFormValue, outputBank.mintDecimals), + priceUnit: quoteString, + symbol: outputBank.name, + triggerPrice: floorToDecimal(triggerPrice, inputBank.mintDecimals), + }) + } else if (inputBank.name === 'USDC') { return t('trade:trigger-order-desc', { amount: floorToDecimal(amountOutFormValue, outputBank.mintDecimals), - symbol: outputBank.name, - triggerPrice: triggerPrice, + orderType: orderTypeString, priceUnit: quoteString, + symbol: outputBank.name, + triggerPrice: floorToDecimal(triggerPrice, inputBank.mintDecimals), }) } else { return t('trade:trigger-order-desc', { amount: floorToDecimal(amountInFormValue, inputBank.mintDecimals), - symbol: inputBank.name, - triggerPrice: triggerPrice, + orderType: orderTypeString, priceUnit: quoteString, + symbol: inputBank.name, + triggerPrice: floorToDecimal(triggerPrice, inputBank.mintDecimals), }) } }, [ @@ -423,6 +448,7 @@ const LimitSwapForm = ({ amountOutFormValue, flipPrices, inputBank, + orderType, outputBank, triggerPrice, ]) @@ -489,13 +515,20 @@ const LimitSwapForm = ({ setOrderType(newType) const triggerMultiplier = newType === OrderTypes.STOP_LOSS - ? 0.9 - : newType === OrderTypes.TAKE_PROFIT ? 1.1 + : newType === OrderTypes.TAKE_PROFIT + ? 0.9 : 1 setOrderTypeMultiplier(triggerMultiplier) const trigger = (quotePrice * triggerMultiplier).toString() setTriggerPrice(trigger) + if (amountInAsDecimal.gt(0)) { + const amountOut = getAmountOut( + amountInAsDecimal.toString(), + trigger, + ).toString() + setAmountOutFormValue(amountOut) + } }, [quotePrice, setOrderTypeMultiplier], ) @@ -619,7 +652,7 @@ const LimitSwapForm = ({ /> {swapFormSizeUi === 'slider' ? ( handleAmountInUi(v)} step={1 / 10 ** (inputBank?.mintDecimals || 6)} @@ -628,7 +661,7 @@ const LimitSwapForm = ({ handleAmountInUi(v)} - useMargin={useMargin} + useMargin={false} /> )} {orderDescription ? ( @@ -636,11 +669,13 @@ const LimitSwapForm = ({ - {inputBank?.name === 'USDC' ? ( - {t('buy')} - ) : ( - {t('sell')} - )}{' '} + {orderType !== OrderTypes.REPAY_BORROW ? ( + inputBank?.name === 'USDC' ? ( + {t('buy')} + ) : ( + {t('sell')} + ) + ) : null}{' '} {orderDescription} } @@ -648,19 +683,31 @@ const LimitSwapForm = ({ /> ) : null} - {orderType === 'repay-borrow' && !hasBorrowToRepay ? ( + {orderType === OrderTypes.REPAY_BORROW && !hasBorrowToRepay ? (
) : null} - + {ipAllowed ? ( + + ) : ( + + )} ) } diff --git a/components/swap/MarketSwapForm.tsx b/components/swap/MarketSwapForm.tsx index 752ceea7..2d3ed6e0 100644 --- a/components/swap/MarketSwapForm.tsx +++ b/components/swap/MarketSwapForm.tsx @@ -6,12 +6,16 @@ import { Dispatch, SetStateAction, } from 'react' -import { ArrowDownIcon } from '@heroicons/react/20/solid' +import { + ArrowDownIcon, + ExclamationCircleIcon, + LinkIcon, +} from '@heroicons/react/20/solid' import { NumberFormatValues, SourceInfo } from 'react-number-format' import Decimal from 'decimal.js' import mangoStore from '@store/mangoStore' import useDebounce from '../shared/useDebounce' -import { SIZE_INPUT_UI_KEY } from '../../utils/constants' +import { MANGO_MINT, SIZE_INPUT_UI_KEY, USDC_MINT } from '../../utils/constants' import { useWallet } from '@solana/wallet-adapter-react' import { RouteInfo } from 'types/jupiter' import useLocalStorageState from 'hooks/useLocalStorageState' @@ -19,11 +23,17 @@ import SwapSlider from './SwapSlider' import PercentageSelectButtons from './PercentageSelectButtons' import BuyTokenInput from './BuyTokenInput' import SellTokenInput from './SellTokenInput' +import Button from '@components/shared/Button' +import { Transition } from '@headlessui/react' +import SwapReviewRouteInfo from './SwapReviewRouteInfo' +import useIpAddress from 'hooks/useIpAddress' +import { useTranslation } from 'react-i18next' +import useQuoteRoutes from './useQuoteRoutes' +import { useTokenMax } from './useTokenMax' +import Loading from '@components/shared/Loading' +import InlineNotification from '@components/shared/InlineNotification' type MarketSwapFormProps = { - bestRoute: RouteInfo | undefined | null - selectedRoute: RouteInfo | undefined | null - setSelectedRoute: Dispatch> setShowTokenSelect: Dispatch> } @@ -39,17 +49,16 @@ export const NUMBER_FORMAT_CLASSNAMES = const set = mangoStore.getState().set -const MarketSwapForm = ({ - bestRoute, - selectedRoute, - setSelectedRoute, - setShowTokenSelect, -}: MarketSwapFormProps) => { +const MarketSwapForm = ({ setShowTokenSelect }: MarketSwapFormProps) => { + 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 [showConfirm, setShowConfirm] = useState(false) const [swapFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider') - const { margin: useMargin, + slippage, inputBank, outputBank, amountIn: amountInFormValue, @@ -58,7 +67,16 @@ const MarketSwapForm = ({ } = mangoStore((s) => s.swap) const [debouncedAmountIn] = useDebounce(amountInFormValue, 300) const [debouncedAmountOut] = useDebounce(amountOutFormValue, 300) - const { connected } = useWallet() + const { connected, publicKey } = useWallet() + const { bestRoute, routes } = useQuoteRoutes({ + inputMint: inputBank?.mint.toString() || USDC_MINT, + outputMint: outputBank?.mint.toString() || MANGO_MINT, + amount: swapMode === 'ExactIn' ? debouncedAmountIn : debouncedAmountOut, + slippage, + swapMode, + wallet: publicKey?.toBase58(), + }) + const { ipAllowed, ipCountry } = useIpAddress() const amountInAsDecimal: Decimal | null = useMemo(() => { return Number(debouncedAmountIn) @@ -197,6 +215,27 @@ const MarketSwapForm = ({ return ( <> +
+ + setShowConfirm(false)} + amountIn={amountInAsDecimal} + slippage={slippage} + routes={routes} + selectedRoute={selectedRoute} + setSelectedRoute={setSelectedRoute} + /> + +
)} + {ipAllowed ? ( + + ) : ( + + )} ) } export default MarketSwapForm + +const SwapFormSubmitButton = ({ + amountIn, + amountOut, + inputSymbol, + loadingSwapDetails, + selectedRoute, + setShowConfirm, + useMargin, +}: { + amountIn: Decimal + amountOut: number | undefined + inputSymbol: string | undefined + loadingSwapDetails: boolean + selectedRoute: RouteInfo | undefined | null + setShowConfirm: (x: boolean) => void + useMargin: boolean +}) => { + const { t } = useTranslation('common') + const { connected, connect } = useWallet() + const { amount: tokenMax, amountWithBorrow } = useTokenMax(useMargin) + + const showInsufficientBalance = useMargin + ? amountWithBorrow.lt(amountIn) + : tokenMax.lt(amountIn) + + const disabled = + connected && + (!amountIn.toNumber() || + showInsufficientBalance || + !amountOut || + !selectedRoute) + + const onClick = connected ? () => setShowConfirm(true) : connect + + return ( + <> + + {selectedRoute === null && amountIn.gt(0) ? ( +
+ +
+ ) : null} + + ) +} diff --git a/components/swap/SwapForm.tsx b/components/swap/SwapForm.tsx index 74348b1d..b060e4c2 100644 --- a/components/swap/SwapForm.tsx +++ b/components/swap/SwapForm.tsx @@ -1,32 +1,16 @@ import { useState, useCallback, useMemo, useEffect } from 'react' import { PublicKey } from '@solana/web3.js' -import { - Cog8ToothIcon, - ExclamationCircleIcon, - LinkIcon, -} from '@heroicons/react/20/solid' -import Decimal from 'decimal.js' +import { Cog8ToothIcon } from '@heroicons/react/20/solid' import mangoStore from '@store/mangoStore' import ContentBox from '../shared/ContentBox' -import SwapReviewRouteInfo from './SwapReviewRouteInfo' -import useDebounce from '../shared/useDebounce' import { useTranslation } from 'next-i18next' import SwapFormTokenList from './SwapFormTokenList' -import { Transition } from '@headlessui/react' -import Button, { IconButton, LinkButton } from '../shared/Button' -import Loading from '../shared/Loading' +import { IconButton, LinkButton } from '../shared/Button' import { EnterBottomExitBottom } from '../shared/Transitions' -import useQuoteRoutes from './useQuoteRoutes' import { HealthType } from '@blockworks-foundation/mango-v4' -import { MANGO_MINT, SWAP_MARGIN_KEY, USDC_MINT } from '../../utils/constants' -import { useTokenMax } from './useTokenMax' +import { SWAP_MARGIN_KEY } from '../../utils/constants' 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 TokenVaultWarnings from '@components/shared/TokenVaultWarnings' -import useIpAddress from 'hooks/useIpAddress' import SwapSettings from './SwapSettings' import InlineNotification from '@components/shared/InlineNotification' import Tooltip from '@components/shared/Tooltip' @@ -40,18 +24,13 @@ const set = mangoStore.getState().set const SwapForm = () => { const { t } = useTranslation(['common', 'swap', 'trade']) - //initial state is undefined null is returned on error - const [selectedRoute, setSelectedRoute] = useState() const [showTokenSelect, setShowTokenSelect] = useState<'input' | 'output'>() const [showSettings, setShowSettings] = useState(false) - const [showConfirm, setShowConfirm] = useState(false) const [swapOrLimit, setSwapOrLimit] = useState('swap') - const { group } = useMangoGroup() const [, setSavedSwapMargin] = useLocalStorageState( SWAP_MARGIN_KEY, true, ) - const { ipAllowed, ipCountry } = useIpAddress() const { margin: useMargin, @@ -60,33 +39,7 @@ const SwapForm = () => { outputBank, amountIn: amountInFormValue, amountOut: amountOutFormValue, - swapMode, } = mangoStore((s) => s.swap) - const [debouncedAmountIn] = useDebounce(amountInFormValue, 300) - const [debouncedAmountOut] = useDebounce(amountOutFormValue, 300) - const { mangoAccount } = useMangoAccount() - const { connected, publicKey } = 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 { bestRoute, routes } = useQuoteRoutes({ - inputMint: inputBank?.mint.toString() || USDC_MINT, - outputMint: outputBank?.mint.toString() || MANGO_MINT, - amount: swapMode === 'ExactIn' ? debouncedAmountIn : debouncedAmountOut, - slippage, - swapMode, - wallet: publicKey?.toBase58(), - }) const handleTokenInSelect = useCallback((mintAddress: string) => { const group = mangoStore.getState().group @@ -114,14 +67,15 @@ const SwapForm = () => { const maintProjectedHealth = useMemo(() => { const group = mangoStore.getState().group + const mangoAccount = mangoStore.getState().mangoAccount.current if ( !inputBank || !mangoAccount || !outputBank || - !amountOutAsDecimal || + !amountOutFormValue || !group ) - return 0 + return 100 const simulatedHealthRatio = mangoAccount.simHealthRatioWithTokenPositionUiChanges( @@ -129,11 +83,11 @@ const SwapForm = () => { [ { mintPk: inputBank.mint, - uiTokenAmount: amountInAsDecimal.toNumber() * -1, + uiTokenAmount: parseFloat(amountInFormValue) * -1, }, { mintPk: outputBank.mint, - uiTokenAmount: amountOutAsDecimal.toNumber(), + uiTokenAmount: parseFloat(amountOutFormValue), }, ], HealthType.maint, @@ -143,28 +97,7 @@ const SwapForm = () => { : simulatedHealthRatio < 0 ? 0 : Math.trunc(simulatedHealthRatio) - }, [ - mangoAccount, - inputBank, - outputBank, - amountInAsDecimal, - amountOutAsDecimal, - ]) - - const loadingSwapDetails: boolean = useMemo(() => { - return ( - !!(amountInAsDecimal.toNumber() || amountOutAsDecimal.toNumber()) && - swapOrLimit === 'swap' && - connected && - typeof selectedRoute === 'undefined' - ) - }, [ - amountInAsDecimal, - amountOutAsDecimal, - connected, - selectedRoute, - swapOrLimit, - ]) + }, [inputBank, outputBank, amountInFormValue, amountOutFormValue]) const handleSwapOrLimit = useCallback( (orderType: string) => { @@ -185,15 +118,15 @@ const SwapForm = () => { const estSlippage = useMemo(() => { const { group } = mangoStore.getState() - if (!group || !inputBank || !amountInAsDecimal.gt(0)) return 0 - const amountIn = amountInAsDecimal.toNumber() + const amountIn = parseFloat(amountInFormValue) || 0 + if (!group || !inputBank || amountIn <= 0) return 0 const value = amountIn * inputBank.uiPrice const slippage = group.getPriceImpactByTokenIndex( inputBank.tokenIndex, value, ) return slippage - }, [amountInAsDecimal, inputBank]) + }, [amountInFormValue, inputBank]) return ( { className="relative overflow-hidden border-x-0 bg-th-bkg-1 md:border-l md:border-r-0 md:border-t-0 md:border-b-0" >
- - setShowConfirm(false)} - amountIn={amountInAsDecimal} - slippage={slippage} - routes={routes} - selectedRoute={selectedRoute} - setSelectedRoute={setSelectedRoute} - /> - {
{swapOrLimit === 'swap' ? ( - + ) : ( )} - {ipAllowed ? ( - swapOrLimit === 'swap' ? ( - - ) : null - ) : ( - - )} - {group && inputBank ? ( + {inputBank ? ( ) : null} {inputBank && @@ -355,7 +239,7 @@ const SwapForm = () => { ) : null} - {estSlippage > 0 && swapOrLimit === 'trade:trigger-order' ? ( + {estSlippage ? ( <>

@@ -392,74 +276,3 @@ const SwapForm = () => { } export default SwapForm - -const SwapFormSubmitButton = ({ - amountIn, - amountOut, - inputSymbol, - loadingSwapDetails, - selectedRoute, - setShowConfirm, - useMargin, -}: { - amountIn: Decimal - amountOut: number | undefined - inputSymbol: string | undefined - loadingSwapDetails: boolean - selectedRoute: RouteInfo | undefined | null - setShowConfirm: (x: boolean) => void - useMargin: boolean -}) => { - const { t } = useTranslation('common') - const { connected, connect } = useWallet() - const { amount: tokenMax, amountWithBorrow } = useTokenMax(useMargin) - - const showInsufficientBalance = useMargin - ? amountWithBorrow.lt(amountIn) - : tokenMax.lt(amountIn) - - const disabled = - connected && - (!amountIn.toNumber() || - showInsufficientBalance || - !amountOut || - !selectedRoute) - - const onClick = connected ? () => setShowConfirm(true) : connect - - return ( - <> - - {selectedRoute === null && amountIn.gt(0) ? ( -

- -
- ) : null} - - ) -} diff --git a/components/swap/SwapTokenChart.tsx b/components/swap/SwapTokenChart.tsx index 8d6b7a56..cbeff815 100644 --- a/components/swap/SwapTokenChart.tsx +++ b/components/swap/SwapTokenChart.tsx @@ -249,26 +249,6 @@ const SwapTokenChart = () => { : `${inputSymbol}/${outputSymbol}` }, [flipPrices, inputBank, inputCoingeckoId, outputBank]) - // const handleFlipPrices = useCallback( - // (flip: boolean) => { - // if (!flipPrices && flip) { - // setSwapChartSettings([ - // ...swapChartSettings, - // { pair: swapMarketName, flipPrices: true }, - // ]) - // } else { - // setSwapChartSettings( - // swapChartSettings.filter( - // (chart: SwapChartSettings) => - // !chart.pair.includes(inputBank!.name) && - // !chart.pair.includes(outputBank!.name), - // ), - // ) - // } - // }, - // [flipPrices, inputBank, outputBank, swapChartSettings, swapMarketName], - // ) - const handleSwapMouseEnter = useCallback( ( swap: SwapHistoryItem | undefined, @@ -460,6 +440,22 @@ const SwapTokenChart = () => { }) }, [coingeckoData, chartSwapTimes]) + const latestChartDataItem = useMemo(() => { + if (!inputBank || !outputBank) return [] + const price = !flipPrices + ? outputBank.uiPrice / inputBank.uiPrice + : inputBank.uiPrice / outputBank.uiPrice + const item: ChartDataItem[] = [ + { + price, + time: Date.now(), + inputTokenPrice: inputBank.uiPrice, + outputTokenPrice: outputBank.uiPrice, + }, + ] + return item + }, [flipPrices, inputBank, outputBank]) + const chartData = useMemo(() => { if (!coingeckoData || !coingeckoData.length || coingeckoData.length < 2) return [] @@ -469,7 +465,10 @@ const SwapTokenChart = () => { const swapPoints = swapHistoryPoints.filter( (point) => point.time >= minTime && point.time <= maxTime, ) - return coingeckoData.concat(swapPoints).sort((a, b) => a.time - b.time) + return coingeckoData + .concat(swapPoints) + .sort((a, b) => a.time - b.time) + .concat(latestChartDataItem) } else return coingeckoData }, [coingeckoData, swapHistoryPoints, showSwaps]) diff --git a/public/locales/en/trade.json b/public/locales/en/trade.json index 612dc7a4..f1674659 100644 --- a/public/locales/en/trade.json +++ b/public/locales/en/trade.json @@ -73,6 +73,7 @@ "realized-pnl": "Realized PnL", "reduce": "Reduce", "reduce-only": "Reduce Only", + "repay-borrow-order-desc": "Repay {{amount}} {{symbol}} if the oracle price reaches {{triggerPrice}} {{priceUnit}}", "sells": "Sells", "settle-funds": "Settle Funds", "settle-funds-error": "Failed to settle funds", @@ -104,7 +105,7 @@ "trades": "Trades", "trigger-price": "Trigger Price", "trigger-order": "Trigger Order", - "trigger-order-desc": "{{amount}} {{symbol}} if the oracle price is {{triggerPrice}} {{priceUnit}}", + "trigger-order-desc": "{{amount}} {{symbol}} if the oracle price {{orderType}} {{triggerPrice}} {{priceUnit}}", "trigger-orders": "Trigger Orders", "tweet-position": "Tweet", "unrealized-pnl": "Unrealized PnL", diff --git a/public/locales/es/trade.json b/public/locales/es/trade.json index 612dc7a4..f1674659 100644 --- a/public/locales/es/trade.json +++ b/public/locales/es/trade.json @@ -73,6 +73,7 @@ "realized-pnl": "Realized PnL", "reduce": "Reduce", "reduce-only": "Reduce Only", + "repay-borrow-order-desc": "Repay {{amount}} {{symbol}} if the oracle price reaches {{triggerPrice}} {{priceUnit}}", "sells": "Sells", "settle-funds": "Settle Funds", "settle-funds-error": "Failed to settle funds", @@ -104,7 +105,7 @@ "trades": "Trades", "trigger-price": "Trigger Price", "trigger-order": "Trigger Order", - "trigger-order-desc": "{{amount}} {{symbol}} if the oracle price is {{triggerPrice}} {{priceUnit}}", + "trigger-order-desc": "{{amount}} {{symbol}} if the oracle price {{orderType}} {{triggerPrice}} {{priceUnit}}", "trigger-orders": "Trigger Orders", "tweet-position": "Tweet", "unrealized-pnl": "Unrealized PnL", diff --git a/public/locales/ru/trade.json b/public/locales/ru/trade.json index 612dc7a4..f1674659 100644 --- a/public/locales/ru/trade.json +++ b/public/locales/ru/trade.json @@ -73,6 +73,7 @@ "realized-pnl": "Realized PnL", "reduce": "Reduce", "reduce-only": "Reduce Only", + "repay-borrow-order-desc": "Repay {{amount}} {{symbol}} if the oracle price reaches {{triggerPrice}} {{priceUnit}}", "sells": "Sells", "settle-funds": "Settle Funds", "settle-funds-error": "Failed to settle funds", @@ -104,7 +105,7 @@ "trades": "Trades", "trigger-price": "Trigger Price", "trigger-order": "Trigger Order", - "trigger-order-desc": "{{amount}} {{symbol}} if the oracle price is {{triggerPrice}} {{priceUnit}}", + "trigger-order-desc": "{{amount}} {{symbol}} if the oracle price {{orderType}} {{triggerPrice}} {{priceUnit}}", "trigger-orders": "Trigger Orders", "tweet-position": "Tweet", "unrealized-pnl": "Unrealized PnL", diff --git a/public/locales/zh/trade.json b/public/locales/zh/trade.json index a59b5c92..db9bf6a0 100644 --- a/public/locales/zh/trade.json +++ b/public/locales/zh/trade.json @@ -72,6 +72,7 @@ "quote": "计价", "reduce": "Reduce", "reduce-only": "限减少", + "repay-borrow-order-desc": "Repay {{amount}} {{symbol}} if the oracle price reaches {{triggerPrice}} {{priceUnit}}", "sells": "卖单", "settle-funds": "借清资金", "settle-funds-error": "借清出错", @@ -102,7 +103,7 @@ "tweet-position": "分享至Twitter", "trigger-price": "Trigger Price", "trigger-order": "Trigger Order", - "trigger-order-desc": "{{amount}} {{symbol}} if the oracle price is {{triggerPrice}} {{priceUnit}}", + "trigger-order-desc": "{{amount}} {{symbol}} if the oracle price {{orderType}} {{triggerPrice}} {{priceUnit}}", "trigger-orders": "Trigger Orders", "unsettled": "未結清", "volume-alert": "交易量警報", diff --git a/public/locales/zh_tw/trade.json b/public/locales/zh_tw/trade.json index 21cd676c..1248276a 100644 --- a/public/locales/zh_tw/trade.json +++ b/public/locales/zh_tw/trade.json @@ -73,6 +73,7 @@ "realized-pnl": "已實現的盈虧", "reduce": "Reduce", "reduce-only": "限減少", + "repay-borrow-order-desc": "Repay {{amount}} {{symbol}} if the oracle price reaches {{triggerPrice}} {{priceUnit}}", "sells": "賣單", "settle-funds": "借清資金", "settle-funds-error": "借清出錯", @@ -104,7 +105,7 @@ "trades": "交易", "trigger-price": "Trigger Price", "trigger-order": "Trigger Order", - "trigger-order-desc": "{{amount}} {{symbol}} if the oracle price is {{triggerPrice}} {{priceUnit}}", + "trigger-order-desc": "{{amount}} {{symbol}} if the oracle price {{orderType}} {{triggerPrice}} {{priceUnit}}", "trigger-orders": "Trigger Orders", "tweet-position": "分享至Twitter", "unrealized-pnl": "未實現盈虧", From 1c85169e3df3460c7ea64ee406c0c8534876466d Mon Sep 17 00:00:00 2001 From: saml33 Date: Fri, 4 Aug 2023 13:29:04 +1000 Subject: [PATCH 21/44] fix chart change mouse data bug --- components/swap/SwapTokenChart.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/swap/SwapTokenChart.tsx b/components/swap/SwapTokenChart.tsx index cbeff815..f2949b6c 100644 --- a/components/swap/SwapTokenChart.tsx +++ b/components/swap/SwapTokenChart.tsx @@ -470,7 +470,7 @@ const SwapTokenChart = () => { .sort((a, b) => a.time - b.time) .concat(latestChartDataItem) } else return coingeckoData - }, [coingeckoData, swapHistoryPoints, showSwaps]) + }, [coingeckoData, latestChartDataItem, swapHistoryPoints, showSwaps]) const handleMouseMove: CategoricalChartFunc = useCallback( (coords) => { @@ -495,6 +495,7 @@ const SwapTokenChart = () => { if (!chartData?.length) return 0 if (mouseData) { const index = chartData.findIndex((d) => d.time === mouseData.time) + if (index === -1) return 0 return ( ((chartData[index]['price'] - chartData[0]['price']) / chartData[0]['price']) * From 07642c4d3b2dc7513441ea438075efe367ec37dc Mon Sep 17 00:00:00 2001 From: saml33 Date: Fri, 4 Aug 2023 16:14:41 +1000 Subject: [PATCH 22/44] add form validation --- components/swap/BuyTokenInput.tsx | 13 ++ components/swap/LimitSwapForm.tsx | 215 ++++++++++++++++++----------- components/swap/SellTokenInput.tsx | 2 +- components/swap/SwapTokenChart.tsx | 2 +- public/locales/en/swap.json | 1 + public/locales/en/trade.json | 1 + public/locales/es/swap.json | 1 + public/locales/es/trade.json | 1 + public/locales/ru/swap.json | 1 + public/locales/ru/trade.json | 1 + public/locales/zh/swap.json | 1 + public/locales/zh/trade.json | 1 + public/locales/zh_tw/swap.json | 1 + public/locales/zh_tw/trade.json | 1 + 14 files changed, 160 insertions(+), 82 deletions(-) diff --git a/components/swap/BuyTokenInput.tsx b/components/swap/BuyTokenInput.tsx index d4c1e79d..6324e44a 100644 --- a/components/swap/BuyTokenInput.tsx +++ b/components/swap/BuyTokenInput.tsx @@ -12,13 +12,16 @@ import mangoStore from '@store/mangoStore' import useMangoGroup from 'hooks/useMangoGroup' import { OUTPUT_TOKEN_DEFAULT } from 'utils/constants' import { NUMBER_FORMAT_CLASSNAMES } from './MarketSwapForm' +import InlineNotification from '@components/shared/InlineNotification' const BuyTokenInput = ({ + error, handleAmountOutChange, loading, setShowTokenSelect, handleRepay, }: { + error?: string handleAmountOutChange: (e: NumberFormatValues, info: SourceInfo) => void loading?: boolean setShowTokenSelect: Dispatch> @@ -94,6 +97,16 @@ const BuyTokenInput = ({ )}
+ {error ? ( +
+ +
+ ) : null} ) } diff --git a/components/swap/LimitSwapForm.tsx b/components/swap/LimitSwapForm.tsx index 9acc2747..19546e47 100644 --- a/components/swap/LimitSwapForm.tsx +++ b/components/swap/LimitSwapForm.tsx @@ -36,6 +36,7 @@ import InlineNotification from '@components/shared/InlineNotification' import { handleFlipPrices } from './SwapTokenChart' import Select from '@components/forms/Select' import useIpAddress from 'hooks/useIpAddress' +import { Bank } from '@blockworks-foundation/mango-v4' type LimitSwapFormProps = { showTokenSelect: 'input' | 'output' | undefined @@ -44,10 +45,14 @@ type LimitSwapFormProps = { 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', @@ -62,6 +67,23 @@ const ORDER_TYPES = [ 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 ? 0.9 : 1.1 + } else if (orderType === OrderTypes.TAKE_PROFIT) { + return flipPrices ? 1.1 : 0.9 + } else { + return 1 + } +} + const LimitSwapForm = ({ showTokenSelect, setShowTokenSelect, @@ -71,7 +93,8 @@ const LimitSwapForm = ({ const [animateSwitchArrow, setAnimateSwitchArrow] = useState(0) const [triggerPrice, setTriggerPrice] = useState('') const [orderType, setOrderType] = useState(ORDER_TYPES[0]) - const [orderTypeMultiplier, setOrderTypeMultiplier] = useState(1.1) + const [orderTypeMultiplier, setOrderTypeMultiplier] = + useState(null) const [submitting, setSubmitting] = useState(false) const [swapFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider') const [formErrors, setFormErrors] = useState({}) @@ -147,13 +170,20 @@ const LimitSwapForm = ({ } }, [inputBank, quotePrice, showTokenSelect, triggerPrice]) - // flip trigger price when chart direction is flipped + // flip trigger price and set amount out when chart direction is flipped useLayoutEffect(() => { if (!quotePrice) return - setTriggerPrice( - (quotePrice * orderTypeMultiplier).toFixed(inputBank?.mintDecimals), - ) - }, [flipPrices, orderTypeMultiplier]) + const multiplier = getOrderTypeMultiplier(orderType, flipPrices) + const decimals = flipPrices + ? outputBank?.mintDecimals + : inputBank?.mintDecimals + const price = (quotePrice * multiplier).toFixed(decimals) + setTriggerPrice(price) + if (amountInAsDecimal?.gt(0)) { + const amountOut = getAmountOut(amountInAsDecimal.toString(), price) + setAmountOutFormValue(amountOut.toString()) + } + }, [flipPrices]) const triggerPriceDifference = useMemo(() => { if (!quotePrice) return 0 @@ -168,21 +198,73 @@ const LimitSwapForm = ({ setTriggerPrice('') } - const isFormValid = useCallback((form: LimitSwapForm) => { - const invalidFields: FormErrors = {} - setFormErrors({}) - const requiredFields: (keyof LimitSwapForm)[] = ['amountIn', 'triggerPrice'] - for (const key of requiredFields) { - const value = form[key] as string - if (!value) { - invalidFields[key] = t('settings:error-required-field') + const hasBorrowToRepay = useMemo(() => { + const mangoAccount = mangoStore.getState().mangoAccount.current + if (orderType !== OrderTypes.REPAY_BORROW || !outputBank || !mangoAccount) + return + const borrow = mangoAccount.getTokenBorrowsUi(outputBank) + return borrow + }, [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], + ) + + // set order type multiplier on page load + useEffect(() => { + if (!orderTypeMultiplier) { + const multiplier = getOrderTypeMultiplier(orderType, flipPrices) + setOrderTypeMultiplier(multiplier) } - if (Object.keys(invalidFields).length) { - setFormErrors(invalidFields) - } - return invalidFields - }, []) + }, [flipPrices, orderType, orderTypeMultiplier]) // If the use margin setting is toggled, clear the form values useEffect(() => { @@ -262,6 +344,7 @@ const LimitSwapForm = ({ 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, triggerPrice) @@ -298,18 +381,19 @@ const LimitSwapForm = ({ 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) * orderTypeMultiplier, + (inputBank.uiPrice / outputBank.uiPrice) * multiplier, outputBank.mintDecimals, ).toString() : floorToDecimal( - (outputBank.uiPrice / inputBank.uiPrice) * orderTypeMultiplier, + (outputBank.uiPrice / inputBank.uiPrice) * multiplier, inputBank.mintDecimals, ).toString() setTriggerPrice(price) @@ -326,7 +410,7 @@ const LimitSwapForm = ({ amountInAsDecimal, flipPrices, inputBank, - orderTypeMultiplier, + orderType, outputBank, triggerPrice, ]) @@ -334,6 +418,7 @@ const LimitSwapForm = ({ const handlePlaceStopLoss = useCallback(async () => { const invalidFields = isFormValid({ amountIn: amountInAsDecimal.toNumber(), + hasBorrows: hasBorrowToRepay, triggerPrice, }) if (Object.keys(invalidFields).length) { @@ -393,6 +478,7 @@ const LimitSwapForm = ({ setSubmitting(false) } }, [ + hasBorrowToRepay, flipPrices, limitPrice, triggerPrice, @@ -465,6 +551,7 @@ const LimitSwapForm = ({ const toggleFlipPrices = useCallback( (flip: boolean) => { if (!inputBank || !outputBank) return + setFormErrors({}) handleFlipPrices( flip, flipPrices, @@ -473,52 +560,24 @@ const LimitSwapForm = ({ swapChartSettings, setSwapChartSettings, ) - const price = !flipPrices - ? floorToDecimal( - (inputBank.uiPrice / outputBank.uiPrice) * orderTypeMultiplier, - outputBank.mintDecimals, - ).toString() - : floorToDecimal( - (outputBank.uiPrice / inputBank.uiPrice) * orderTypeMultiplier, - inputBank.mintDecimals, - ).toString() - setTriggerPrice(price) }, [ + getOrderTypeMultiplier, flipPrices, inputBank, - orderTypeMultiplier, + orderType, outputBank, swapChartSettings, setSwapChartSettings, ], ) - const hasBorrowToRepay = useMemo(() => { - const mangoAccount = mangoStore.getState().mangoAccount.current - if (orderType !== OrderTypes.REPAY_BORROW || !outputBank || !mangoAccount) - return - const borrow = mangoAccount.getTokenBorrowsUi(outputBank) - return borrow - }, [orderType, outputBank]) - - const sellTokenBalance = useMemo(() => { - const mangoAccount = mangoStore.getState().mangoAccount.current - if (!inputBank || !mangoAccount) return 0 - const balance = mangoAccount.getTokenBalanceUi(inputBank) - return balance - }, [inputBank]) - const handleOrderTypeChange = useCallback( (type: string) => { + setFormErrors({}) const newType = type as OrderTypes setOrderType(newType) - const triggerMultiplier = - newType === OrderTypes.STOP_LOSS - ? 1.1 - : newType === OrderTypes.TAKE_PROFIT - ? 0.9 - : 1 + const triggerMultiplier = getOrderTypeMultiplier(newType, flipPrices) setOrderTypeMultiplier(triggerMultiplier) const trigger = (quotePrice * triggerMultiplier).toString() setTriggerPrice(trigger) @@ -530,16 +589,16 @@ const LimitSwapForm = ({ setAmountOutFormValue(amountOut) } }, - [quotePrice, setOrderTypeMultiplier], + [flipPrices, quotePrice, 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) + // 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 ( <> @@ -618,17 +677,17 @@ const LimitSwapForm = ({ {triggerPriceSuffix} - {formErrors.triggerPrice ? ( -
- -
- ) : null} + {formErrors.triggerPrice ? ( +
+ +
+ ) : null}
handleTokenSelect('output')} handleRepay={handleRepay} @@ -664,7 +724,8 @@ const LimitSwapForm = ({ useMargin={false} /> )} - {orderDescription ? ( + {orderType === OrderTypes.REPAY_BORROW && + !hasBorrowToRepay ? null : orderDescription ? (
) : null} - {orderType === OrderTypes.REPAY_BORROW && !hasBorrowToRepay ? ( -
- -
- ) : null} {ipAllowed ? (
+ From 64bc709034ad3d4fb68254951e05d779b0f52094 Mon Sep 17 00:00:00 2001 From: saml33 Date: Wed, 9 Aug 2023 13:14:59 +1000 Subject: [PATCH 30/44] fix order desc for flip prices --- components/swap/LimitSwapForm.tsx | 32 +++++++++++-------------------- components/swap/SwapForm.tsx | 1 + public/locales/en/trade.json | 4 +++- public/locales/es/trade.json | 4 +++- public/locales/ru/trade.json | 4 +++- public/locales/zh/trade.json | 2 ++ public/locales/zh_tw/trade.json | 2 ++ 7 files changed, 25 insertions(+), 24 deletions(-) diff --git a/components/swap/LimitSwapForm.tsx b/components/swap/LimitSwapForm.tsx index 74736239..4c6ee4b7 100644 --- a/components/swap/LimitSwapForm.tsx +++ b/components/swap/LimitSwapForm.tsx @@ -607,15 +607,10 @@ const LimitSwapForm = ({ ) return - const quoteString = !flipPrices + const quoteString = flipPrices ? `${inputBankName} per ${outputBankName}` : `${outputBankName} per ${inputBankName}` - const orderTypeString = - orderType === OrderTypes.STOP_LOSS - ? t('trade:falls-to') - : t('trade:rises-to') - if (orderType === OrderTypes.REPAY_BORROW) { return t('trade:repay-borrow-order-desc', { amount: floorToDecimal(amountOutFormValue, outputBankDecimals), @@ -623,15 +618,16 @@ const LimitSwapForm = ({ symbol: outputBankName, triggerPrice: floorToDecimal(triggerPrice, inputBankDecimals), }) - } else if (inputBankName === 'USDC') { - return t('trade:trigger-order-desc', { - amount: floorToDecimal(amountOutFormValue, outputBankDecimals), - orderType: orderTypeString, - 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, @@ -842,13 +838,7 @@ const LimitSwapForm = ({ - {orderType !== OrderTypes.REPAY_BORROW ? ( - inputBankName === 'USDC' ? ( - {t('buy')} - ) : ( - {t('sell')} - ) - ) : null}{' '} + {t('sell')}{' '} {orderDescription} } diff --git a/components/swap/SwapForm.tsx b/components/swap/SwapForm.tsx index 9473d683..b882cafc 100644 --- a/components/swap/SwapForm.tsx +++ b/components/swap/SwapForm.tsx @@ -74,6 +74,7 @@ const SwapForm = () => { !inputBank || !mangoAccount || !outputBank || + !amountInFormValue || !amountOutFormValue || !group ) diff --git a/public/locales/en/trade.json b/public/locales/en/trade.json index 03217a47..2cef54ef 100644 --- a/public/locales/en/trade.json +++ b/public/locales/en/trade.json @@ -2,6 +2,7 @@ "24h-volume": "24h Volume", "activate-volume-alert": "Activate Volume Alert", "amount": "Amount", + "avg-entry-price": "Avg. Entry Price", "average-funding": "Average {{interval}} Funding", "average-price-of": "at an average price of", "base": "Base", @@ -19,8 +20,8 @@ "depth": "Depth", "edit-order": "Edit Order", "est-liq-price": "Est. Liq. Price", - "avg-entry-price": "Avg. Entry Price", "est-slippage": "Est. Slippage", + "falls-to": "falls to", "filled": "Filled", "for": "for", "funding-limits": "Funding Limits", @@ -74,6 +75,7 @@ "reduce": "Reduce", "reduce-only": "Reduce Only", "repay-borrow-order-desc": "Repay {{amount}} {{symbol}} if the oracle price reaches {{triggerPrice}} {{priceUnit}}", + "rises-to": "rises to", "sells": "Sells", "settle-funds": "Settle Funds", "settle-funds-error": "Failed to settle funds", diff --git a/public/locales/es/trade.json b/public/locales/es/trade.json index 03217a47..2cef54ef 100644 --- a/public/locales/es/trade.json +++ b/public/locales/es/trade.json @@ -2,6 +2,7 @@ "24h-volume": "24h Volume", "activate-volume-alert": "Activate Volume Alert", "amount": "Amount", + "avg-entry-price": "Avg. Entry Price", "average-funding": "Average {{interval}} Funding", "average-price-of": "at an average price of", "base": "Base", @@ -19,8 +20,8 @@ "depth": "Depth", "edit-order": "Edit Order", "est-liq-price": "Est. Liq. Price", - "avg-entry-price": "Avg. Entry Price", "est-slippage": "Est. Slippage", + "falls-to": "falls to", "filled": "Filled", "for": "for", "funding-limits": "Funding Limits", @@ -74,6 +75,7 @@ "reduce": "Reduce", "reduce-only": "Reduce Only", "repay-borrow-order-desc": "Repay {{amount}} {{symbol}} if the oracle price reaches {{triggerPrice}} {{priceUnit}}", + "rises-to": "rises to", "sells": "Sells", "settle-funds": "Settle Funds", "settle-funds-error": "Failed to settle funds", diff --git a/public/locales/ru/trade.json b/public/locales/ru/trade.json index 03217a47..2cef54ef 100644 --- a/public/locales/ru/trade.json +++ b/public/locales/ru/trade.json @@ -2,6 +2,7 @@ "24h-volume": "24h Volume", "activate-volume-alert": "Activate Volume Alert", "amount": "Amount", + "avg-entry-price": "Avg. Entry Price", "average-funding": "Average {{interval}} Funding", "average-price-of": "at an average price of", "base": "Base", @@ -19,8 +20,8 @@ "depth": "Depth", "edit-order": "Edit Order", "est-liq-price": "Est. Liq. Price", - "avg-entry-price": "Avg. Entry Price", "est-slippage": "Est. Slippage", + "falls-to": "falls to", "filled": "Filled", "for": "for", "funding-limits": "Funding Limits", @@ -74,6 +75,7 @@ "reduce": "Reduce", "reduce-only": "Reduce Only", "repay-borrow-order-desc": "Repay {{amount}} {{symbol}} if the oracle price reaches {{triggerPrice}} {{priceUnit}}", + "rises-to": "rises to", "sells": "Sells", "settle-funds": "Settle Funds", "settle-funds-error": "Failed to settle funds", diff --git a/public/locales/zh/trade.json b/public/locales/zh/trade.json index 5339e4f1..c3ed5145 100644 --- a/public/locales/zh/trade.json +++ b/public/locales/zh/trade.json @@ -21,6 +21,7 @@ "est-liq-price": "Est. Liq. Price", "avg-entry-price": "Avg. Entry Price", "est-slippage": "Est. Slippage", + "falls-to": "falls to", "filled": "Filled", "for": "for", "funding-limits": "Funding Limits", @@ -73,6 +74,7 @@ "reduce": "Reduce", "reduce-only": "限减少", "repay-borrow-order-desc": "Repay {{amount}} {{symbol}} if the oracle price reaches {{triggerPrice}} {{priceUnit}}", + "rises-to": "rises to", "sells": "卖单", "settle-funds": "借清资金", "settle-funds-error": "借清出错", diff --git a/public/locales/zh_tw/trade.json b/public/locales/zh_tw/trade.json index 87cebe52..664aa92d 100644 --- a/public/locales/zh_tw/trade.json +++ b/public/locales/zh_tw/trade.json @@ -21,6 +21,7 @@ "edit-order": "編輯訂單", "est-liq-price": "預計清算價格", "est-slippage": "預計下滑", + "falls-to": "falls to", "filled": "Filled", "for": "for", "funding-limits": "資金費限制", @@ -74,6 +75,7 @@ "reduce": "Reduce", "reduce-only": "限減少", "repay-borrow-order-desc": "Repay {{amount}} {{symbol}} if the oracle price reaches {{triggerPrice}} {{priceUnit}}", + "rises-to": "rises to", "sells": "賣單", "settle-funds": "借清資金", "settle-funds-error": "借清出錯", From 621839af2525f583e0466aa14678262835190767 Mon Sep 17 00:00:00 2001 From: saml33 Date: Wed, 9 Aug 2023 13:40:26 +1000 Subject: [PATCH 31/44] add cancel all --- components/swap/SwapOrders.tsx | 58 ++++++++++++++++++++++++++++++--- public/locales/en/trade.json | 1 + public/locales/es/trade.json | 1 + public/locales/ru/trade.json | 1 + public/locales/zh/trade.json | 1 + public/locales/zh_tw/trade.json | 1 + 6 files changed, 59 insertions(+), 4 deletions(-) diff --git a/components/swap/SwapOrders.tsx b/components/swap/SwapOrders.tsx index 0421a927..e644e490 100644 --- a/components/swap/SwapOrders.tsx +++ b/components/swap/SwapOrders.tsx @@ -1,4 +1,4 @@ -import { IconButton } from '@components/shared/Button' +import { IconButton, LinkButton } from '@components/shared/Button' import ConnectEmptyState from '@components/shared/ConnectEmptyState' import { SortableColumnHeader, @@ -133,6 +133,48 @@ const SwapOrders = () => { }) } } + } catch (e) { + console.error('failed to cancel trigger order', e) + } finally { + setCancelId('') + } + } + + const handleCancelAll = async () => { + try { + const client = mangoStore.getState().client + const group = mangoStore.getState().group + const actions = mangoStore.getState().actions + const mangoAccount = mangoStore.getState().mangoAccount.current + + if (!mangoAccount || !group) return + setCancelId('all') + + try { + const tx = await client.tokenConditionalSwapCancelAll( + group, + mangoAccount, + ) + notify({ + title: 'Transaction confirmed', + type: 'success', + txid: tx, + noSound: true, + }) + actions.fetchGroup() + await actions.reloadMangoAccount() + } catch (e) { + console.error('failed to cancel trigger orders', e) + sentry.captureException(e) + if (isMangoError(e)) { + notify({ + title: 'Transaction failed', + description: e.message, + txid: e?.txid, + type: 'error', + }) + } + } } catch (e) { console.error('failed to cancel swap order', e) } finally { @@ -212,7 +254,13 @@ const SwapOrders = () => { /> - + @@ -273,11 +321,13 @@ const SwapOrders = () => {
+
+ requestSort('side')} + sortConfig={sortConfig} + title={t('trade:side')} + /> +
+
{ fee, pair, sellBank, + side, size, filled, triggerPrice, } = data + + const bank = side === 'buy' ? buyBank : sellBank return (
{pair} +
+ +
+

{size} - - {' '} - {sellBank.name} - + {bank.name}

{filled}/{size} - - {' '} - {sellBank.name} - + {bank.name}

@@ -240,7 +264,7 @@ const SwapOrders = () => { {triggerPrice} {' '} - {buyBank.name} + {side === 'buy' ? sellBank.name : buyBank.name}

{t('cancel')} +
+ + {t('trade:cancel-all')} + +
+
handleCancel(data.id)} size="small" > - {cancelId === data.id.toString() ? ( + {cancelId === data.id.toString() || cancelId === 'all' ? ( ) : ( diff --git a/public/locales/en/trade.json b/public/locales/en/trade.json index 2cef54ef..848651cc 100644 --- a/public/locales/en/trade.json +++ b/public/locales/en/trade.json @@ -8,6 +8,7 @@ "base": "Base", "book": "Book", "buys": "Buys", + "cancel-all": "Cancel All", "cancel-order-error": "Failed to cancel order", "close-confirm": "Market close your {{config_name}} position", "close-position": "Close Position", diff --git a/public/locales/es/trade.json b/public/locales/es/trade.json index 2cef54ef..848651cc 100644 --- a/public/locales/es/trade.json +++ b/public/locales/es/trade.json @@ -8,6 +8,7 @@ "base": "Base", "book": "Book", "buys": "Buys", + "cancel-all": "Cancel All", "cancel-order-error": "Failed to cancel order", "close-confirm": "Market close your {{config_name}} position", "close-position": "Close Position", diff --git a/public/locales/ru/trade.json b/public/locales/ru/trade.json index 2cef54ef..848651cc 100644 --- a/public/locales/ru/trade.json +++ b/public/locales/ru/trade.json @@ -8,6 +8,7 @@ "base": "Base", "book": "Book", "buys": "Buys", + "cancel-all": "Cancel All", "cancel-order-error": "Failed to cancel order", "close-confirm": "Market close your {{config_name}} position", "close-position": "Close Position", diff --git a/public/locales/zh/trade.json b/public/locales/zh/trade.json index c3ed5145..3e2dd443 100644 --- a/public/locales/zh/trade.json +++ b/public/locales/zh/trade.json @@ -7,6 +7,7 @@ "base": "Base", "book": "Book", "buys": "Buys", + "cancel-all": "Cancel All", "cancel-order-error": "Failed to cancel order", "close-confirm": "Market close your {{config_name}} position", "close-position": "Close Position", diff --git a/public/locales/zh_tw/trade.json b/public/locales/zh_tw/trade.json index 664aa92d..12e52c58 100644 --- a/public/locales/zh_tw/trade.json +++ b/public/locales/zh_tw/trade.json @@ -8,6 +8,7 @@ "base": "基礎", "book": "單薄", "buys": "買", + "cancel-all": "Cancel All", "cancel-order-error": "取消訂單失敗", "close-confirm": "市場平倉 {{config_name}}", "close-position": "平倉", From 317c7579c21d8005004c2d2fea9237a39d9061fb Mon Sep 17 00:00:00 2001 From: saml33 Date: Wed, 9 Aug 2023 14:31:49 +1000 Subject: [PATCH 32/44] improve repay borrow order description --- components/swap/BuyTokenInput.tsx | 4 +-- components/swap/LimitSwapForm.tsx | 52 ++++++++++++++++++++++++------- public/locales/en/trade.json | 1 + public/locales/es/trade.json | 1 + public/locales/ru/trade.json | 1 + public/locales/zh/trade.json | 1 + public/locales/zh_tw/trade.json | 1 + 7 files changed, 48 insertions(+), 13 deletions(-) diff --git a/components/swap/BuyTokenInput.tsx b/components/swap/BuyTokenInput.tsx index ef4ffe42..af3277ed 100644 --- a/components/swap/BuyTokenInput.tsx +++ b/components/swap/BuyTokenInput.tsx @@ -26,7 +26,7 @@ const BuyTokenInput = ({ handleAmountOutChange: (e: NumberFormatValues, info: SourceInfo) => void loading?: boolean setShowTokenSelect: Dispatch> - handleRepay: (amountOut: string) => void + handleRepay?: (amountOut: string) => void }) => { const { t } = useTranslation('common') const { mangoAccount } = useMangoAccount() @@ -49,7 +49,7 @@ const BuyTokenInput = ({

{t('buy')}

- {outputTokenBalanceBorrow ? ( + {handleRepay && outputTokenBalanceBorrow ? ( { setShowTokenSelect(type) + setFormErrors({}) setTriggerPrice('') } const hasBorrowToRepay = useMemo(() => { if (orderType !== OrderTypes.REPAY_BORROW || !outputBank || !mangoAccount) return - const borrow = mangoAccount.getTokenBorrowsUi(outputBank) - return borrow + 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( @@ -611,13 +616,31 @@ const LimitSwapForm = ({ ? `${inputBankName} per ${outputBankName}` : `${outputBankName} per ${inputBankName}` - if (orderType === OrderTypes.REPAY_BORROW) { - return t('trade:repay-borrow-order-desc', { - amount: floorToDecimal(amountOutFormValue, outputBankDecimals), - priceUnit: quoteString, - symbol: outputBankName, - triggerPrice: floorToDecimal(triggerPrice, inputBankDecimals), - }) + 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 @@ -640,6 +663,7 @@ const LimitSwapForm = ({ amountInFormValue, amountOutFormValue, flipPrices, + hasBorrowToRepay, inputBankDecimals, inputBankName, orderType, @@ -816,7 +840,9 @@ const LimitSwapForm = ({ error={formErrors.hasBorrows} handleAmountOutChange={handleAmountOutChange} setShowTokenSelect={() => handleTokenSelect('output')} - handleRepay={handleRepay} + handleRepay={ + orderType === OrderTypes.REPAY_BORROW ? handleRepay : undefined + } /> {swapFormSizeUi === 'slider' ? ( - {t('sell')}{' '} + {orderType !== OrderTypes.REPAY_BORROW ? ( + <> + {t('sell')}{' '} + + ) : null} {orderDescription} } diff --git a/public/locales/en/trade.json b/public/locales/en/trade.json index 848651cc..54d813f8 100644 --- a/public/locales/en/trade.json +++ b/public/locales/en/trade.json @@ -76,6 +76,7 @@ "reduce": "Reduce", "reduce-only": "Reduce Only", "repay-borrow-order-desc": "Repay {{amount}} {{symbol}} if the oracle price reaches {{triggerPrice}} {{priceUnit}}", + "repay-borrow-deposit-order-desc": "Repay {{borrowAmount}} and buy {{depositAmount}} {{symbol}} if the oracle price reaches {{triggerPrice}} {{priceUnit}}", "rises-to": "rises to", "sells": "Sells", "settle-funds": "Settle Funds", diff --git a/public/locales/es/trade.json b/public/locales/es/trade.json index 848651cc..54d813f8 100644 --- a/public/locales/es/trade.json +++ b/public/locales/es/trade.json @@ -76,6 +76,7 @@ "reduce": "Reduce", "reduce-only": "Reduce Only", "repay-borrow-order-desc": "Repay {{amount}} {{symbol}} if the oracle price reaches {{triggerPrice}} {{priceUnit}}", + "repay-borrow-deposit-order-desc": "Repay {{borrowAmount}} and buy {{depositAmount}} {{symbol}} if the oracle price reaches {{triggerPrice}} {{priceUnit}}", "rises-to": "rises to", "sells": "Sells", "settle-funds": "Settle Funds", diff --git a/public/locales/ru/trade.json b/public/locales/ru/trade.json index 848651cc..54d813f8 100644 --- a/public/locales/ru/trade.json +++ b/public/locales/ru/trade.json @@ -76,6 +76,7 @@ "reduce": "Reduce", "reduce-only": "Reduce Only", "repay-borrow-order-desc": "Repay {{amount}} {{symbol}} if the oracle price reaches {{triggerPrice}} {{priceUnit}}", + "repay-borrow-deposit-order-desc": "Repay {{borrowAmount}} and buy {{depositAmount}} {{symbol}} if the oracle price reaches {{triggerPrice}} {{priceUnit}}", "rises-to": "rises to", "sells": "Sells", "settle-funds": "Settle Funds", diff --git a/public/locales/zh/trade.json b/public/locales/zh/trade.json index 3e2dd443..5d6a498d 100644 --- a/public/locales/zh/trade.json +++ b/public/locales/zh/trade.json @@ -75,6 +75,7 @@ "reduce": "Reduce", "reduce-only": "限减少", "repay-borrow-order-desc": "Repay {{amount}} {{symbol}} if the oracle price reaches {{triggerPrice}} {{priceUnit}}", + "repay-borrow-deposit-order-desc": "Repay {{borrowAmount}} and buy {{depositAmount}} {{symbol}} if the oracle price reaches {{triggerPrice}} {{priceUnit}}", "rises-to": "rises to", "sells": "卖单", "settle-funds": "借清资金", diff --git a/public/locales/zh_tw/trade.json b/public/locales/zh_tw/trade.json index 12e52c58..f0c3c900 100644 --- a/public/locales/zh_tw/trade.json +++ b/public/locales/zh_tw/trade.json @@ -76,6 +76,7 @@ "reduce": "Reduce", "reduce-only": "限減少", "repay-borrow-order-desc": "Repay {{amount}} {{symbol}} if the oracle price reaches {{triggerPrice}} {{priceUnit}}", + "repay-borrow-deposit-order-desc": "Repay {{borrowAmount}} and buy {{depositAmount}} {{symbol}} if the oracle price reaches {{triggerPrice}} {{priceUnit}}", "rises-to": "rises to", "sells": "賣單", "settle-funds": "借清資金", From 07974f75bfbca348f5d7769d37903c19a9f512f8 Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Thu, 10 Aug 2023 16:17:32 +0200 Subject: [PATCH 33/44] Bump client Signed-off-by: microwavedcola1 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 6b566c19..54326057 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ }, "dependencies": { "@blockworks-foundation/mango-feeds": "0.1.7", - "@blockworks-foundation/mango-v4": "^0.18.14", + "@blockworks-foundation/mango-v4": "^0.18.15", "@headlessui/react": "1.6.6", "@heroicons/react": "2.0.10", "@metaplex-foundation/js": "0.19.4", diff --git a/yarn.lock b/yarn.lock index cf316dd2..771f1a55 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26,10 +26,10 @@ dependencies: ws "^8.13.0" -"@blockworks-foundation/mango-v4@^0.18.14": - version "0.18.14" - resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4/-/mango-v4-0.18.14.tgz#29179616aa1cf453aadda47fd81cf60e1b1266a3" - integrity sha512-QOoSoPAdEBhr6H+ZI424TMQRKYW3k2rz+5hiwB1xwZrSJJrtFS6V0r14bCYtXF73jS+kl7bauKlz+cTAMqvy2Q== +"@blockworks-foundation/mango-v4@^0.18.15": + version "0.18.15" + resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4/-/mango-v4-0.18.15.tgz#d77c4cfc9d421c93e42531978cb0972aff324973" + integrity sha512-pn/e+EqfFwfrciYx3efQpvyZaUdMcy66OJ/b0Anq6N4cda/6PYmkprg3GZHUGiB3o/H3i0eFxNOnu88i/uwQzQ== dependencies: "@coral-xyz/anchor" "^0.27.0" "@project-serum/serum" "0.13.65" From 25e885a0844b8deadb482155c7f8c4ab25799b66 Mon Sep 17 00:00:00 2001 From: saml33 Date: Fri, 11 Aug 2023 09:45:40 +1000 Subject: [PATCH 34/44] add free collateral warning --- components/swap/MaxSwapAmount.tsx | 2 +- components/swap/SwapForm.tsx | 23 ++++++++++++++++++++++- 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 + 7 files changed, 28 insertions(+), 2 deletions(-) diff --git a/components/swap/MaxSwapAmount.tsx b/components/swap/MaxSwapAmount.tsx index 1d162599..ebf06675 100644 --- a/components/swap/MaxSwapAmount.tsx +++ b/components/swap/MaxSwapAmount.tsx @@ -33,7 +33,7 @@ const MaxSwapAmount = ({ setMax(tokenMax)} value={tokenMax} /> diff --git a/components/swap/SwapForm.tsx b/components/swap/SwapForm.tsx index b882cafc..e4a26c67 100644 --- a/components/swap/SwapForm.tsx +++ b/components/swap/SwapForm.tsx @@ -7,7 +7,10 @@ import { useTranslation } from 'next-i18next' import SwapFormTokenList from './SwapFormTokenList' import { IconButton, LinkButton } from '../shared/Button' import { EnterBottomExitBottom } from '../shared/Transitions' -import { HealthType } from '@blockworks-foundation/mango-v4' +import { + HealthType, + toUiDecimalsForQuote, +} from '@blockworks-foundation/mango-v4' import { SWAP_MARGIN_KEY } from '../../utils/constants' import HealthImpact from '@components/shared/HealthImpact' import TokenVaultWarnings from '@components/shared/TokenVaultWarnings' @@ -20,11 +23,13 @@ import LimitSwapForm from './LimitSwapForm' import Switch from '@components/forms/Switch' import useLocalStorageState from 'hooks/useLocalStorageState' import { useIsWhiteListed } from 'hooks/useIsWhiteListed' +import useMangoAccount from 'hooks/useMangoAccount' const set = mangoStore.getState().set const SwapForm = () => { const { t } = useTranslation(['common', 'swap', 'trade']) + const { mangoAccountAddress } = useMangoAccount() const { data: isWhiteListed } = useIsWhiteListed() const [showTokenSelect, setShowTokenSelect] = useState<'input' | 'output'>() const [showSettings, setShowSettings] = useState(false) @@ -131,6 +136,14 @@ const SwapForm = () => { return slippage }, [amountInFormValue, inputBank]) + const freeCollateral = useMemo(() => { + const group = mangoStore.getState().group + const mangoAccount = mangoStore.getState().mangoAccount.current + return group && mangoAccount + ? toUiDecimalsForQuote(mangoAccount.getCollateralValue(group)) + : 0 + }, [mangoAccountAddress]) + return ( { />
) : null} + {freeCollateral <= 0 ? ( +
+ +
+ ) : null}
Date: Fri, 11 Aug 2023 10:52:42 +1000 Subject: [PATCH 35/44] add deposit funds button when no balance or collateral --- components/swap/LimitSwapForm.tsx | 63 +++++++++++++++++++++++++++--- components/swap/MarketSwapForm.tsx | 48 +++++++++++++++++------ components/swap/SellTokenInput.tsx | 25 +++++++++++- components/swap/SwapForm.tsx | 23 +---------- 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 + 9 files changed, 123 insertions(+), 41 deletions(-) diff --git a/components/swap/LimitSwapForm.tsx b/components/swap/LimitSwapForm.tsx index 9f8daf1b..b0fe718f 100644 --- a/components/swap/LimitSwapForm.tsx +++ b/components/swap/LimitSwapForm.tsx @@ -7,7 +7,12 @@ import { SetStateAction, useLayoutEffect, } from 'react' -import { ArrowDownIcon, ArrowsRightLeftIcon } from '@heroicons/react/20/solid' +import { + ArrowDownIcon, + ArrowDownTrayIcon, + ArrowsRightLeftIcon, + LinkIcon, +} from '@heroicons/react/20/solid' import NumberFormat, { NumberFormatValues, SourceInfo, @@ -36,8 +41,11 @@ 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 { Bank, toUiDecimalsForQuote } from '@blockworks-foundation/mango-v4' import useMangoAccount from 'hooks/useMangoAccount' +import { useWallet } from '@solana/wallet-adapter-react' +import { useTokenMax } from './useTokenMax' +import DepositWithdrawModal from '@components/modals/DepositWithdrawModal' type LimitSwapFormProps = { showTokenSelect: 'input' | 'output' | undefined @@ -90,7 +98,7 @@ const LimitSwapForm = ({ setShowTokenSelect, }: LimitSwapFormProps) => { const { t } = useTranslation(['common', 'swap', 'trade']) - const { mangoAccount } = useMangoAccount() + const { mangoAccount, mangoAccountAddress } = useMangoAccount() const { ipAllowed, ipCountry } = useIpAddress() const [animateSwitchArrow, setAnimateSwitchArrow] = useState(0) const [triggerPrice, setTriggerPrice] = useState('') @@ -112,6 +120,10 @@ const LimitSwapForm = ({ amountOut: amountOutFormValue, } = mangoStore((s) => s.swap) + const { connected, connect } = useWallet() + const { amount: tokenMax } = useTokenMax() + const [showDepositModal, setShowDepositModal] = useState(false) + const [inputBankName, outputBankName, inputBankDecimals, outputBankDecimals] = useMemo(() => { if (!inputBank || !outputBank) return ['', '', 0, 0] @@ -135,6 +147,17 @@ const LimitSwapForm = ({ : new Decimal(0) }, [amountOutFormValue]) + const freeCollateral = useMemo(() => { + const group = mangoStore.getState().group + const mangoAccount = mangoStore.getState().mangoAccount.current + return group && mangoAccount + ? toUiDecimalsForQuote(mangoAccount.getCollateralValue(group)) + : 0 + }, [mangoAccountAddress]) + + const showInsufficientBalance = + tokenMax.lt(amountInAsDecimal) || tokenMax.eq(0) + const flipPrices = useMemo(() => { if (!swapChartSettings.length || !inputBankName || !outputBankName) return false @@ -731,6 +754,12 @@ const LimitSwapForm = ({ // parseFloat(triggerPrice) < quotePrice) || // amountInAsDecimal.gt(sellTokenBalance) + const onClick = !connected + ? connect + : showInsufficientBalance || freeCollateral <= 0 + ? () => setShowDepositModal(true) + : () => handlePlaceStopLoss + return ( <> - {submitting ? : t('swap:place-limit-order')} + {connected ? ( + showInsufficientBalance || freeCollateral <= 0 ? ( +
+ + {t('swap:deposit-funds')} +
+ ) : submitting ? ( + + ) : ( + {t('swap:place-limit-order')} + ) + ) : ( +
+ + {t('connect')} +
+ )} ) : (
) : null} + {showDepositModal ? ( + setShowDepositModal(false)} + token={freeCollateral > 0 ? inputSymbol : ''} + /> + ) : null} ) } diff --git a/components/swap/SellTokenInput.tsx b/components/swap/SellTokenInput.tsx index ef67c572..6aa5cc67 100644 --- a/components/swap/SellTokenInput.tsx +++ b/components/swap/SellTokenInput.tsx @@ -5,7 +5,7 @@ import NumberFormat, { } from 'react-number-format' import { formatCurrencyValue } from 'utils/numbers' import { useTranslation } from 'react-i18next' -import { Dispatch, SetStateAction } from 'react' +import { Dispatch, SetStateAction, useMemo } from 'react' import mangoStore from '@store/mangoStore' import useMangoGroup from 'hooks/useMangoGroup' import { INPUT_TOKEN_DEFAULT } from 'utils/constants' @@ -13,6 +13,9 @@ import { NUMBER_FORMAT_CLASSNAMES, withValueLimit } from './MarketSwapForm' import MaxSwapAmount from './MaxSwapAmount' import useUnownedAccount from 'hooks/useUnownedAccount' import InlineNotification from '@components/shared/InlineNotification' +import useMangoAccount from 'hooks/useMangoAccount' +import { useWallet } from '@solana/wallet-adapter-react' +import { toUiDecimalsForQuote } from '@blockworks-foundation/mango-v4' const SellTokenInput = ({ handleAmountInChange, @@ -30,6 +33,8 @@ const SellTokenInput = ({ isTriggerOrder?: boolean }) => { const { t } = useTranslation('common') + const { mangoAccountAddress } = useMangoAccount() + const { connected } = useWallet() const { group } = useMangoGroup() const { isUnownedAccount } = useUnownedAccount() const { @@ -38,6 +43,14 @@ const SellTokenInput = ({ amountIn: amountInFormValue, } = mangoStore((s) => s.swap) + const freeCollateral = useMemo(() => { + const group = mangoStore.getState().group + const mangoAccount = mangoStore.getState().mangoAccount.current + return group && mangoAccount + ? toUiDecimalsForQuote(mangoAccount.getCollateralValue(group)) + : 0 + }, [mangoAccountAddress]) + return (
@@ -83,6 +96,16 @@ const SellTokenInput = ({ ) : null}
+ {connected && freeCollateral <= 0 ? ( +
+ +
+ ) : null} {error ? (
{ const { t } = useTranslation(['common', 'swap', 'trade']) - const { mangoAccountAddress } = useMangoAccount() const { data: isWhiteListed } = useIsWhiteListed() const [showTokenSelect, setShowTokenSelect] = useState<'input' | 'output'>() const [showSettings, setShowSettings] = useState(false) @@ -136,14 +131,6 @@ const SwapForm = () => { return slippage }, [amountInFormValue, inputBank]) - const freeCollateral = useMemo(() => { - const group = mangoStore.getState().group - const mangoAccount = mangoStore.getState().mangoAccount.current - return group && mangoAccount - ? toUiDecimalsForQuote(mangoAccount.getCollateralValue(group)) - : 0 - }, [mangoAccountAddress]) - return ( { />
) : null} - {freeCollateral <= 0 ? ( -
- -
- ) : null}
Date: Fri, 11 Aug 2023 11:05:14 +1000 Subject: [PATCH 36/44] fix z index on status bar --- components/StatusBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/StatusBar.tsx b/components/StatusBar.tsx index 3e6dff93..242efc2f 100644 --- a/components/StatusBar.tsx +++ b/components/StatusBar.tsx @@ -46,7 +46,7 @@ const StatusBar = ({ collapsed }: { collapsed: boolean }) => {
From eef5d86e1afda7ecdcc0808b31da1be8f54eb64b Mon Sep 17 00:00:00 2001 From: saml33 Date: Fri, 11 Aug 2023 19:56:01 +1000 Subject: [PATCH 37/44] fix place order button --- components/swap/LimitSwapForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/swap/LimitSwapForm.tsx b/components/swap/LimitSwapForm.tsx index b0fe718f..227132e5 100644 --- a/components/swap/LimitSwapForm.tsx +++ b/components/swap/LimitSwapForm.tsx @@ -758,7 +758,7 @@ const LimitSwapForm = ({ ? connect : showInsufficientBalance || freeCollateral <= 0 ? () => setShowDepositModal(true) - : () => handlePlaceStopLoss + : handlePlaceStopLoss return ( <> From 903a0482c1b2023c41662f04c0540e1e511db574 Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Fri, 11 Aug 2023 16:48:37 +0200 Subject: [PATCH 38/44] Bump client Signed-off-by: microwavedcola1 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 4f4fd3e4..1fe0171e 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ }, "dependencies": { "@blockworks-foundation/mango-feeds": "0.1.7", - "@blockworks-foundation/mango-v4": "^0.18.15", + "@blockworks-foundation/mango-v4": "^0.18.17", "@blockworks-foundation/mango-v4-settings": "0.2.6", "@headlessui/react": "1.6.6", "@heroicons/react": "2.0.10", diff --git a/yarn.lock b/yarn.lock index 53a589f8..09576382 100644 --- a/yarn.lock +++ b/yarn.lock @@ -34,10 +34,10 @@ bn.js "^5.2.1" eslint-config-prettier "^9.0.0" -"@blockworks-foundation/mango-v4@^0.18.15": - version "0.18.15" - resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4/-/mango-v4-0.18.15.tgz#d77c4cfc9d421c93e42531978cb0972aff324973" - integrity sha512-pn/e+EqfFwfrciYx3efQpvyZaUdMcy66OJ/b0Anq6N4cda/6PYmkprg3GZHUGiB3o/H3i0eFxNOnu88i/uwQzQ== +"@blockworks-foundation/mango-v4@^0.18.17": + version "0.18.17" + resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4/-/mango-v4-0.18.17.tgz#e6ed1895df7c5ebdcae2ded04fd219f2964323a7" + integrity sha512-LT2rgFumxESRjHXZ7bke9A/6OK5Y8yMZLEH8z2kF8nzAtQP7RPB42pVTY2l5vjHKYAz+Wo73WwdrpoLqjeXL/w== dependencies: "@coral-xyz/anchor" "^0.27.0" "@project-serum/serum" "0.13.65" From 5aefb6b49f9c815afec633aab190af5a3f7ffc3f Mon Sep 17 00:00:00 2001 From: saml33 Date: Sun, 13 Aug 2023 22:47:25 +1000 Subject: [PATCH 39/44] use helper function for current price --- components/swap/SwapOrders.tsx | 7 ++-- package.json | 2 +- yarn.lock | 61 ++++++---------------------------- 3 files changed, 16 insertions(+), 54 deletions(-) diff --git a/components/swap/SwapOrders.tsx b/components/swap/SwapOrders.tsx index 2fbd843a..ac96e981 100644 --- a/components/swap/SwapOrders.tsx +++ b/components/swap/SwapOrders.tsx @@ -70,9 +70,10 @@ const SwapOrders = () => { const triggerPrice = order.getThresholdPriceUi(group) const pricePremium = order.getPricePremium() const filled = order.getSoldUi(group) - const currentPrice = (sellBank.uiPrice / buyBank.uiPrice).toFixed( - buyBank.mintDecimals, - ) + // const currentPrice = (sellBank.uiPrice / buyBank.uiPrice).toFixed( + // buyBank.mintDecimals, + // ) + const currentPrice = order.getCurrentPairPriceUi(group) const data = { ...order, diff --git a/package.json b/package.json index 3e69c2a3..d0d52870 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ }, "dependencies": { "@blockworks-foundation/mango-feeds": "0.1.7", - "@blockworks-foundation/mango-v4": "^0.19.2", + "@blockworks-foundation/mango-v4": "^0.19.3", "@blockworks-foundation/mango-v4-settings": "0.2.6", "@headlessui/react": "1.6.6", "@heroicons/react": "2.0.10", diff --git a/yarn.lock b/yarn.lock index 71f82988..24c613ed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,14 +12,7 @@ resolved "https://registry.yarnpkg.com/@apocentre/alias-sampling/-/alias-sampling-0.5.3.tgz#897ff181b48ad7b2bcb4ecf29400214888244f08" integrity sha512-7UDWIIF9hIeJqfKXkNIzkVandlwLf1FWTSdrb9iXvOP8oF544JRXQjCbiTmCv2c9n44n/FIWtehhBfNuAx2CZA== -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.5", "@babel/runtime@^7.17.2", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.7", "@babel/runtime@^7.22.5", "@babel/runtime@^7.22.6": - version "7.22.6" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.6.tgz#57d64b9ae3cff1d67eb067ae117dac087f5bd438" - integrity sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ== - dependencies: - regenerator-runtime "^0.13.11" - -"@babel/runtime@^7.21.0": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.5", "@babel/runtime@^7.17.2", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5", "@babel/runtime@^7.22.6": version "7.22.10" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.10.tgz#ae3e9631fd947cb7e3610d3e9d8fef5f76696682" integrity sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ== @@ -41,10 +34,10 @@ bn.js "^5.2.1" eslint-config-prettier "^9.0.0" -"@blockworks-foundation/mango-v4@^0.19.2": - version "0.19.2" - resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4/-/mango-v4-0.19.2.tgz#2047fdffd43b326ce48f7a774077f89c27906618" - integrity sha512-mEKzpr1IIjb7AO3JKuIK6kvLDAQW/qenDW+Pdr+OCvnlADdHWDR0CJ8MByDVatnlGHw4jJi/eqn8gBYll+8/LQ== +"@blockworks-foundation/mango-v4@^0.19.3": + version "0.19.3" + resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4/-/mango-v4-0.19.3.tgz#c41b002a674b6ca7647a0c6ec007ffd705d1a53a" + integrity sha512-E+0yVLEkomrSuCrAu8odmBb/hZOOt8h4izDqrKC+Af+fg/n8hCylAax+Lt0fbtSgp4V3l4xNPCPHUkTiMA5zOQ== dependencies: "@coral-xyz/anchor" "^0.27.0" "@project-serum/serum" "0.13.65" @@ -1009,31 +1002,24 @@ jsbi "^3.1.5" sha.js "^2.4.11" -"@noble/curves@1.1.0", "@noble/curves@^1.1.0", "@noble/curves@~1.1.0": +"@noble/curves@1.1.0", "@noble/curves@^1.0.0", "@noble/curves@^1.1.0", "@noble/curves@~1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.1.0.tgz#f13fc667c89184bc04cccb9b11e8e7bae27d8c3d" integrity sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA== dependencies: "@noble/hashes" "1.3.1" -"@noble/curves@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.0.0.tgz#e40be8c7daf088aaf291887cbc73f43464a92932" - integrity sha512-2upgEu0iLiDVDZkNLeFV2+ht0BAVgQnEmCk6JsOch9Rp8xfkMCbvbAZlA2pBHQc73dbl+vFOXfqkf4uemdn0bw== - dependencies: - "@noble/hashes" "1.3.0" - "@noble/ed25519@^1.6.1", "@noble/ed25519@^1.7.1": version "1.7.3" resolved "https://registry.yarnpkg.com/@noble/ed25519/-/ed25519-1.7.3.tgz#57e1677bf6885354b466c38e2b620c62f45a7123" integrity sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ== -"@noble/hashes@1.3.0", "@noble/hashes@^1.1.3", "@noble/hashes@^1.3.0": +"@noble/hashes@1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.0.tgz#085fd70f6d7d9d109671090ccae1d3bec62554a1" integrity sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg== -"@noble/hashes@1.3.1", "@noble/hashes@~1.3.0", "@noble/hashes@~1.3.1": +"@noble/hashes@1.3.1", "@noble/hashes@^1.1.3", "@noble/hashes@^1.3.0", "@noble/hashes@~1.3.0", "@noble/hashes@~1.3.1": version "1.3.1" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9" integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA== @@ -1578,7 +1564,7 @@ dependencies: "@solana/wallet-adapter-base" "^0.9.23" -"@solana/wallet-adapter-base@0.9.23", "@solana/wallet-adapter-base@^0.9.22", "@solana/wallet-adapter-base@^0.9.23": +"@solana/wallet-adapter-base@0.9.23", "@solana/wallet-adapter-base@^0.9.17", "@solana/wallet-adapter-base@^0.9.2", "@solana/wallet-adapter-base@^0.9.22", "@solana/wallet-adapter-base@^0.9.23": version "0.9.23" resolved "https://registry.yarnpkg.com/@solana/wallet-adapter-base/-/wallet-adapter-base-0.9.23.tgz#3b17c28afd44e173f44f658bf9700fd637e12a11" integrity sha512-apqMuYwFp1jFi55NxDfvXUX2x1T0Zh07MxhZ/nCCTGys5raSfYUh82zen2BLv8BSDj/JxZ2P/s7jrQZGrX8uAw== @@ -1588,16 +1574,6 @@ "@wallet-standard/features" "^1.0.3" eventemitter3 "^4.0.7" -"@solana/wallet-adapter-base@^0.9.17", "@solana/wallet-adapter-base@^0.9.2": - version "0.9.22" - resolved "https://registry.yarnpkg.com/@solana/wallet-adapter-base/-/wallet-adapter-base-0.9.22.tgz#97812eaf6aebe01e5fe714326b3c9a0614ae6112" - integrity sha512-xbLEZPGSJFvgTeldG9D55evhl7QK/3e/F7vhvcA97mEt1eieTgeKMnGlmmjs3yivI3/gtZNZeSk1XZLnhKcQvw== - dependencies: - "@solana/wallet-standard-features" "^1.0.1" - "@wallet-standard/base" "^1.0.1" - "@wallet-standard/features" "^1.0.3" - eventemitter3 "^4.0.7" - "@solana/wallet-adapter-bitkeep@^0.3.19": version "0.3.19" resolved "https://registry.yarnpkg.com/@solana/wallet-adapter-bitkeep/-/wallet-adapter-bitkeep-0.3.19.tgz#7d6a467fae791c5486b325473ed6b494958e3b5e" @@ -1983,15 +1959,7 @@ dependencies: "@wallet-standard/base" "^1.0.1" -"@solana/wallet-standard-features@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@solana/wallet-standard-features/-/wallet-standard-features-1.0.1.tgz#36270a646f74a80e51b9e21fb360edb64f840c68" - integrity sha512-SUfx7KtBJ55XIj0qAhhVcC1I6MklAXqWFEz9hDHW+6YcJIyvfix/EilBhaBik1FJ2JT0zukpOfFv8zpuAbFRbw== - dependencies: - "@wallet-standard/base" "^1.0.1" - "@wallet-standard/features" "^1.0.3" - -"@solana/wallet-standard-features@^1.1.0": +"@solana/wallet-standard-features@^1.0.1", "@solana/wallet-standard-features@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@solana/wallet-standard-features/-/wallet-standard-features-1.1.0.tgz#516d78626dd0802d299db49298e4ebbec3433940" integrity sha512-oVyygxfYkkF5INYL0GuD8GFmNO/wd45zNesIqGCFE6X66BYxmI6HmyzQJCcZTZ0BNsezlVg4t+3MCL5AhfFoGA== @@ -7822,14 +7790,7 @@ semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.3.2, semver@^7.3.5, semver@^7.3.7: - version "7.3.8" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" - integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== - dependencies: - lru-cache "^6.0.0" - -semver@^7.3.8: +semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== From 574d51bd409a31f0671363c7631b58171466168a Mon Sep 17 00:00:00 2001 From: saml33 Date: Mon, 14 Aug 2023 11:30:28 +1000 Subject: [PATCH 40/44] add beta label --- components/shared/TabUnderline.tsx | 9 +++++++- components/swap/SwapForm.tsx | 33 ++++++++++++++++-------------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/components/shared/TabUnderline.tsx b/components/shared/TabUnderline.tsx index 44ebd92f..8810b2b8 100644 --- a/components/shared/TabUnderline.tsx +++ b/components/shared/TabUnderline.tsx @@ -67,7 +67,14 @@ const TabUnderline = ({ `} key={`${value}` + i} > - {names ? names[i] : t(`${value}`)} + + {names ? names[i] : t(`${value}`)} + {value === 'trade:trigger-order' ? ( + + beta + + ) : null} + ))} diff --git a/components/swap/SwapForm.tsx b/components/swap/SwapForm.tsx index b882cafc..3471d8d8 100644 --- a/components/swap/SwapForm.tsx +++ b/components/swap/SwapForm.tsx @@ -1,11 +1,11 @@ import { useState, useCallback, useMemo, useEffect } from 'react' import { PublicKey } from '@solana/web3.js' -import { Cog8ToothIcon } from '@heroicons/react/20/solid' +import { PencilIcon } from '@heroicons/react/20/solid' import mangoStore from '@store/mangoStore' import ContentBox from '../shared/ContentBox' import { useTranslation } from 'next-i18next' import SwapFormTokenList from './SwapFormTokenList' -import { IconButton, LinkButton } from '../shared/Button' +import { LinkButton } from '../shared/Button' import { EnterBottomExitBottom } from '../shared/Transitions' import { HealthType } from '@blockworks-foundation/mango-v4' import { SWAP_MARGIN_KEY } from '../../utils/constants' @@ -160,7 +160,7 @@ const SwapForm = () => {
{isWhiteListed ? ( -
+
{ />
) : null} -
- setShowSettings(true)} - > - - -
{swapOrLimit === 'swap' ? ( - + <> + {/*
+ setShowSettings(true)} + > + + +
*/} + + ) : ( { {t('swap:max-slippage')}

setShowSettings(true)} > - {slippage}% + {slippage}% +
From 038cd7439690abe7d0195c3acac7ed325369fa5a Mon Sep 17 00:00:00 2001 From: saml33 Date: Wed, 16 Aug 2023 15:47:02 +1000 Subject: [PATCH 41/44] add trigger orders table to account page --- components/account/AccountTabs.tsx | 36 +++++++++++++++++++++++++++++- components/swap/SwapInfoTabs.tsx | 8 ++----- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/components/account/AccountTabs.tsx b/components/account/AccountTabs.tsx index 8c27439c..6d9c8ffe 100644 --- a/components/account/AccountTabs.tsx +++ b/components/account/AccountTabs.tsx @@ -12,29 +12,61 @@ import useOpenPerpPositions from 'hooks/useOpenPerpPositions' import OpenOrders from '@components/trade/OpenOrders' import HistoryTabs from './HistoryTabs' import ManualRefresh from '@components/shared/ManualRefresh' +import useMangoAccount from 'hooks/useMangoAccount' +import { useIsWhiteListed } from 'hooks/useIsWhiteListed' +import SwapOrders from '@components/swap/SwapOrders' const AccountTabs = () => { const [activeTab, setActiveTab] = useState('balances') + const { mangoAccount } = useMangoAccount() const { width } = useViewport() const unsettledSpotBalances = useUnsettledSpotBalances() const unsettledPerpPositions = useUnsettledPerpPositions() const openPerpPositions = useOpenPerpPositions() const openOrders = mangoStore((s) => s.mangoAccount.openOrders) const isMobile = width ? width < breakpoints.lg : false + const { data: isWhiteListed } = useIsWhiteListed() + + // const tabsWithCount: [string, number][] = useMemo(() => { + // let tabs: [string, number][] = [ + // ['balances', 0], + // ['swap:swap-history', 0], + // ] + // if (isWhiteListed) { + // const stopOrdersCount = + // mangoAccount?.tokenConditionalSwaps.filter((tcs) => tcs.hasData) + // ?.length || 0 + // tabs = [ + // ['balances', 0], + // ['trade:trigger-orders', stopOrdersCount], + // ['swap:swap-history', 0], + // ] + // } + // return tabs + // }, [isWhiteListed, mangoAccount]) const tabsWithCount: [string, number][] = useMemo(() => { const unsettledTradeCount = Object.values(unsettledSpotBalances).flat().length + unsettledPerpPositions?.length - return [ + const tabs: [string, number][] = [ ['balances', 0], ['trade:positions', openPerpPositions.length], ['trade:orders', Object.values(openOrders).flat().length], ['trade:unsettled', unsettledTradeCount], ['history', 0], ] + if (isWhiteListed) { + const stopOrdersCount = + mangoAccount?.tokenConditionalSwaps.filter((tcs) => tcs.hasData) + ?.length || 0 + tabs.splice(3, 0, ['trade:trigger-orders', stopOrdersCount]) + } + return tabs }, [ + isWhiteListed, + mangoAccount, openPerpPositions, unsettledPerpPositions, unsettledSpotBalances, @@ -72,6 +104,8 @@ const TabContent = ({ activeTab }: { activeTab: string }) => { return case 'trade:orders': return + case 'trade:trigger-orders': + return case 'trade:unsettled': return ( { const isMobile = width ? width < breakpoints.lg : false const tabsWithCount: [string, number][] = useMemo(() => { - let tabs: [string, number][] = [ + const tabs: [string, number][] = [ ['balances', 0], ['swap:swap-history', 0], ] @@ -25,11 +25,7 @@ const SwapInfoTabs = () => { const stopOrdersCount = mangoAccount?.tokenConditionalSwaps.filter((tcs) => tcs.hasData) ?.length || 0 - tabs = [ - ['balances', 0], - ['trade:trigger-orders', stopOrdersCount], - ['swap:swap-history', 0], - ] + tabs.splice(1, 0, ['trade:trigger-orders', stopOrdersCount]) } return tabs }, [isWhiteListed, mangoAccount]) From 0b86592ae4bec25086507eae9fd9ac360897d53d Mon Sep 17 00:00:00 2001 From: saml33 Date: Thu, 17 Aug 2023 08:31:47 +1000 Subject: [PATCH 42/44] add latest price to chart, remove comments, update copy --- components/account/AccountTabs.tsx | 18 -------------- components/swap/LimitSwapForm.tsx | 8 ------ components/swap/SwapForm.tsx | 30 +---------------------- components/swap/SwapOrders.tsx | 10 +------- components/swap/SwapTokenChart.tsx | 39 +++++++++++++++--------------- public/locales/en/trade.json | 4 +-- public/locales/es/trade.json | 4 +-- public/locales/ru/trade.json | 4 +-- public/locales/zh/trade.json | 4 +-- public/locales/zh_tw/trade.json | 4 +-- 10 files changed, 32 insertions(+), 93 deletions(-) diff --git a/components/account/AccountTabs.tsx b/components/account/AccountTabs.tsx index 6d9c8ffe..48bca7ae 100644 --- a/components/account/AccountTabs.tsx +++ b/components/account/AccountTabs.tsx @@ -27,24 +27,6 @@ const AccountTabs = () => { const isMobile = width ? width < breakpoints.lg : false const { data: isWhiteListed } = useIsWhiteListed() - // const tabsWithCount: [string, number][] = useMemo(() => { - // let tabs: [string, number][] = [ - // ['balances', 0], - // ['swap:swap-history', 0], - // ] - // if (isWhiteListed) { - // const stopOrdersCount = - // mangoAccount?.tokenConditionalSwaps.filter((tcs) => tcs.hasData) - // ?.length || 0 - // tabs = [ - // ['balances', 0], - // ['trade:trigger-orders', stopOrdersCount], - // ['swap:swap-history', 0], - // ] - // } - // return tabs - // }, [isWhiteListed, mangoAccount]) - const tabsWithCount: [string, number][] = useMemo(() => { const unsettledTradeCount = Object.values(unsettledSpotBalances).flat().length + diff --git a/components/swap/LimitSwapForm.tsx b/components/swap/LimitSwapForm.tsx index 70a448fe..b7c5d1b4 100644 --- a/components/swap/LimitSwapForm.tsx +++ b/components/swap/LimitSwapForm.tsx @@ -746,14 +746,6 @@ const LimitSwapForm = ({ [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) - const onClick = !connected ? connect : showInsufficientBalance || freeCollateral <= 0 diff --git a/components/swap/SwapForm.tsx b/components/swap/SwapForm.tsx index 3471d8d8..671c2367 100644 --- a/components/swap/SwapForm.tsx +++ b/components/swap/SwapForm.tsx @@ -169,18 +169,7 @@ const SwapForm = () => {
) : null} {swapOrLimit === 'swap' ? ( - <> - {/*
- setShowSettings(true)} - > - - -
*/} - - + ) : ( { {estSlippage.toFixed(2)}%
- {/* {outputBank ? ( -
-

- {t('swap:est-received')} -

- - {floorToDecimal( - amountOutAsDecimal.div(1 + estSlippage / 100), - outputBank.mintDecimals, - ).toNumber()} - - {' '} - {outputBank?.name} - - -
- ) : null} */} ) : null}
diff --git a/components/swap/SwapOrders.tsx b/components/swap/SwapOrders.tsx index ac96e981..3b63aba6 100644 --- a/components/swap/SwapOrders.tsx +++ b/components/swap/SwapOrders.tsx @@ -15,21 +15,18 @@ import mangoStore from '@store/mangoStore' import useMangoAccount from 'hooks/useMangoAccount' import useMangoGroup from 'hooks/useMangoGroup' import { useSortableData } from 'hooks/useSortableData' -// import { useViewport } from 'hooks/useViewport' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { notify } from 'utils/notifications' import { floorToDecimal } from 'utils/numbers' -// import { breakpoints } from 'utils/theme' import * as sentry from '@sentry/nextjs' import { isMangoError } from 'types' import Loading from '@components/shared/Loading' import SideBadge from '@components/shared/SideBadge' +// todo: add mobile table const SwapOrders = () => { const { t } = useTranslation(['common', 'swap', 'trade']) - // const { width } = useViewport() - // const showTableView = width ? width > breakpoints.md : false const { mangoAccount, mangoAccountAddress } = useMangoAccount() const { group } = useMangoGroup() const { connected } = useWallet() @@ -40,8 +37,6 @@ const SwapOrders = () => { return mangoAccount.tokenConditionalSwaps.filter((tcs) => tcs.hasData) }, [mangoAccount]) - console.log(orders) - const formattedTableData = useCallback(() => { if (!group) return [] const formatted = [] @@ -70,9 +65,6 @@ const SwapOrders = () => { const triggerPrice = order.getThresholdPriceUi(group) const pricePremium = order.getPricePremium() const filled = order.getSoldUi(group) - // const currentPrice = (sellBank.uiPrice / buyBank.uiPrice).toFixed( - // buyBank.mintDecimals, - // ) const currentPrice = order.getCurrentPairPriceUi(group) const data = { diff --git a/components/swap/SwapTokenChart.tsx b/components/swap/SwapTokenChart.tsx index 76bfc722..9728b35c 100644 --- a/components/swap/SwapTokenChart.tsx +++ b/components/swap/SwapTokenChart.tsx @@ -454,21 +454,21 @@ const SwapTokenChart = () => { }) }, [coingeckoData, chartSwapTimes]) - // const latestChartDataItem = useMemo(() => { - // if (!inputBank || !outputBank) return [] - // const price = !flipPrices - // ? outputBank.uiPrice / inputBank.uiPrice - // : inputBank.uiPrice / outputBank.uiPrice - // const item: ChartDataItem[] = [ - // { - // price, - // time: Date.now(), - // inputTokenPrice: inputBank.uiPrice, - // outputTokenPrice: outputBank.uiPrice, - // }, - // ] - // return item - // }, [flipPrices, inputBank, outputBank]) + const latestChartDataItem = useMemo(() => { + if (!inputBank || !outputBank) return [] + const price = flipPrices + ? outputBank.uiPrice / inputBank.uiPrice + : inputBank.uiPrice / outputBank.uiPrice + const item: ChartDataItem[] = [ + { + price, + time: Date.now(), + inputTokenPrice: inputBank.uiPrice, + outputTokenPrice: outputBank.uiPrice, + }, + ] + return item + }, [flipPrices, inputBank, outputBank]) const chartData = useMemo(() => { if (!coingeckoData || !coingeckoData.length || coingeckoData.length < 2) @@ -479,10 +479,11 @@ const SwapTokenChart = () => { const swapPoints = swapHistoryPoints.filter( (point) => point.time >= minTime && point.time <= maxTime, ) - return coingeckoData.concat(swapPoints).sort((a, b) => a.time - b.time) - // .concat(latestChartDataItem) - } else return coingeckoData - // .concat(latestChartDataItem) + return coingeckoData + .concat(swapPoints) + .sort((a, b) => a.time - b.time) + .concat(latestChartDataItem) + } else return coingeckoData.concat(latestChartDataItem) }, [coingeckoData, swapHistoryPoints, showSwaps]) const handleMouseMove: CategoricalChartFunc = useCallback( diff --git a/public/locales/en/trade.json b/public/locales/en/trade.json index 3173d14f..7430648b 100644 --- a/public/locales/en/trade.json +++ b/public/locales/en/trade.json @@ -90,9 +90,9 @@ "spread": "Spread", "stable-price": "Stable Price", "stop-limit": "Stop Limit", - "stop-loss": "Stop-Loss", + "stop-loss": "Stop Loss", "stop-market": "Stop Market", - "take-profit": "Take-Profit", + "take-profit": "Take Profit", "taker": "Taker", "taker-fee": "Taker Fee", "tick-size": "Tick Size", diff --git a/public/locales/es/trade.json b/public/locales/es/trade.json index 3173d14f..7430648b 100644 --- a/public/locales/es/trade.json +++ b/public/locales/es/trade.json @@ -90,9 +90,9 @@ "spread": "Spread", "stable-price": "Stable Price", "stop-limit": "Stop Limit", - "stop-loss": "Stop-Loss", + "stop-loss": "Stop Loss", "stop-market": "Stop Market", - "take-profit": "Take-Profit", + "take-profit": "Take Profit", "taker": "Taker", "taker-fee": "Taker Fee", "tick-size": "Tick Size", diff --git a/public/locales/ru/trade.json b/public/locales/ru/trade.json index 3173d14f..7430648b 100644 --- a/public/locales/ru/trade.json +++ b/public/locales/ru/trade.json @@ -90,9 +90,9 @@ "spread": "Spread", "stable-price": "Stable Price", "stop-limit": "Stop Limit", - "stop-loss": "Stop-Loss", + "stop-loss": "Stop Loss", "stop-market": "Stop Market", - "take-profit": "Take-Profit", + "take-profit": "Take Profit", "taker": "Taker", "taker-fee": "Taker Fee", "tick-size": "Tick Size", diff --git a/public/locales/zh/trade.json b/public/locales/zh/trade.json index af3ebf7e..c7cedf2c 100644 --- a/public/locales/zh/trade.json +++ b/public/locales/zh/trade.json @@ -87,9 +87,9 @@ "show-bids": "显示出价", "side": "方向", "stop-limit": "Stop Limit", - "stop-loss": "Stop-Loss", + "stop-loss": "Stop Loss", "stop-market": "Stop Market", - "take-profit": "Take-Profit", + "take-profit": "Take Profit", "trigger-price": "Trigger Price", "trigger-order": "Trigger Order", "trigger-order-desc": "{{amount}} {{symbol}} if the oracle price {{orderType}} {{triggerPrice}} {{priceUnit}}", diff --git a/public/locales/zh_tw/trade.json b/public/locales/zh_tw/trade.json index ffdb05b7..a80a004f 100644 --- a/public/locales/zh_tw/trade.json +++ b/public/locales/zh_tw/trade.json @@ -90,9 +90,9 @@ "spread": "差價", "stable-price": "穩定價格", "stop-limit": "Stop Limit", - "stop-loss": "Stop-Loss", + "stop-loss": "Stop Loss", "stop-market": "Stop Market", - "take-profit": "Take-Profit", + "take-profit": "Take Profit", "taker": "吃單者", "taker-fee": "吃單者費用", "tick-size": "波動單位", From 31dc29e05f5c4b85ea01a446ffb6264ff09e18c7 Mon Sep 17 00:00:00 2001 From: saml33 Date: Thu, 17 Aug 2023 12:58:35 +1000 Subject: [PATCH 43/44] remove saving chart settings and fix latest price chart flicker --- apis/coingecko.ts | 19 ++++- components/swap/LimitSwapForm.tsx | 42 ++-------- components/swap/SwapForm.tsx | 2 - components/swap/SwapTokenChart.tsx | 118 +++++------------------------ store/mangoStore.ts | 4 +- types/index.ts | 6 -- utils/constants.ts | 2 - 7 files changed, 45 insertions(+), 148 deletions(-) diff --git a/apis/coingecko.ts b/apis/coingecko.ts index 6de38c06..d809cec8 100644 --- a/apis/coingecko.ts +++ b/apis/coingecko.ts @@ -1,3 +1,5 @@ +import { Bank } from '@blockworks-foundation/mango-v4' + type CoingeckoOhlcv = [ time: number, open: number, @@ -15,7 +17,9 @@ export type ChartDataItem = { export const fetchChartData = async ( baseTokenId: string | undefined, + inputBank: Bank | undefined, quoteTokenId: string | undefined, + outputBank: Bank | undefined, daysToShow: string, flipPrices: boolean, ): Promise => { @@ -50,7 +54,20 @@ export const fetchChartData = async ( }) } } - return parsedData + if (inputBank && outputBank) { + const latestPrice = flipPrices + ? outputBank.uiPrice / inputBank.uiPrice + : inputBank.uiPrice / outputBank.uiPrice + const item: ChartDataItem[] = [ + { + price: latestPrice, + time: Date.now(), + inputTokenPrice: inputBank.uiPrice, + outputTokenPrice: outputBank.uiPrice, + }, + ] + return parsedData.concat(item) + } else return parsedData } else { return [] } diff --git a/components/swap/LimitSwapForm.tsx b/components/swap/LimitSwapForm.tsx index b7c5d1b4..478ff39b 100644 --- a/components/swap/LimitSwapForm.tsx +++ b/components/swap/LimitSwapForm.tsx @@ -20,10 +20,7 @@ import NumberFormat, { 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 { SIZE_INPUT_UI_KEY } from '../../utils/constants' import useLocalStorageState from 'hooks/useLocalStorageState' import SwapSlider from './SwapSlider' import PercentageSelectButtons from './PercentageSelectButtons' @@ -38,7 +35,6 @@ 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, toUiDecimalsForQuote } from '@blockworks-foundation/mango-v4' @@ -108,16 +104,13 @@ const LimitSwapForm = ({ 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, + flipPrices, } = mangoStore((s) => s.swap) const { connected, connect } = useWallet() @@ -158,19 +151,6 @@ const LimitSwapForm = ({ const showInsufficientBalance = tokenMax.lt(amountInAsDecimal) || tokenMax.eq(0) - 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 @@ -708,21 +688,11 @@ const LimitSwapForm = ({ (flip: boolean) => { if (!inputBankName || !outputBankName) return setFormErrors({}) - handleFlipPrices( - flip, - inputBankName, - outputBankName, - swapChartSettings, - setSwapChartSettings, - ) + set((state) => { + state.swap.flipPrices = flip + }) }, - [ - inputBankName, - outputBankName, - setFormErrors, - setSwapChartSettings, - swapChartSettings, - ], + [inputBankName, outputBankName, setFormErrors], ) const handleOrderTypeChange = useCallback( diff --git a/components/swap/SwapForm.tsx b/components/swap/SwapForm.tsx index 671c2367..0c4c1131 100644 --- a/components/swap/SwapForm.tsx +++ b/components/swap/SwapForm.tsx @@ -49,7 +49,6 @@ const SwapForm = () => { const bank = group.getFirstBankByMint(new PublicKey(mintAddress)) set((s) => { s.swap.inputBank = bank - s.swap.limitPrice = '' }) } setShowTokenSelect(undefined) @@ -61,7 +60,6 @@ const SwapForm = () => { const bank = group.getFirstBankByMint(new PublicKey(mintAddress)) set((s) => { s.swap.outputBank = bank - s.swap.limitPrice = '' }) } setShowTokenSelect(undefined) diff --git a/components/swap/SwapTokenChart.tsx b/components/swap/SwapTokenChart.tsx index 9728b35c..dad7da8a 100644 --- a/components/swap/SwapTokenChart.tsx +++ b/components/swap/SwapTokenChart.tsx @@ -32,10 +32,7 @@ import { ChartDataItem, fetchChartData } from 'apis/coingecko' import mangoStore from '@store/mangoStore' import useJupiterSwapData from './useJupiterSwapData' import useLocalStorageState from 'hooks/useLocalStorageState' -import { - ANIMATION_SETTINGS_KEY, - SWAP_CHART_SETTINGS_KEY, -} from 'utils/constants' +import { ANIMATION_SETTINGS_KEY } from 'utils/constants' import { INITIAL_ANIMATION_SETTINGS } from '@components/settings/AnimationSettings' import { useTranslation } from 'next-i18next' import { @@ -49,57 +46,13 @@ import { CategoricalChartFunc } from 'recharts/types/chart/generateCategoricalCh import { interpolateNumber } from 'd3-interpolate' import { IconButton } from '@components/shared/Button' import Tooltip from '@components/shared/Tooltip' -import { SwapChartSettings, SwapHistoryItem } from 'types' +import { SwapHistoryItem } from 'types' import useThemeWrapper from 'hooks/useThemeWrapper' import FavoriteSwapButton from './FavoriteSwapButton' dayjs.extend(relativeTime) -export const getChartPairSettings = ( - swapChartSettings: SwapChartSettings[], - inputToken: string, - outputToken: string, -) => { - const pairSettings = swapChartSettings.find( - (chart: SwapChartSettings) => - chart.pair.includes(inputToken) && chart.pair.includes(outputToken), - ) - return pairSettings -} - -export const handleFlipPrices = ( - flip: boolean, - inputToken: string | undefined, - outputToken: string | undefined, - swapChartSettings: SwapChartSettings[], - setSwapChartSettings: (settings: SwapChartSettings[]) => void, -) => { - if (!inputToken || !outputToken) return - - const pairSettings = getChartPairSettings( - swapChartSettings, - inputToken, - outputToken, - ) - - const base = flip ? outputToken : inputToken - const quote = flip ? inputToken : outputToken - - if (pairSettings) { - pairSettings.base = base - pairSettings.quote = quote - setSwapChartSettings([...swapChartSettings]) - } else { - setSwapChartSettings([ - ...swapChartSettings, - { - pair: `${outputToken}/${inputToken}`, - base: base, - quote: quote, - }, - ]) - } -} +const set = mangoStore.getState().set const CustomizedLabel = ({ chartData, @@ -225,8 +178,7 @@ const SwapHistoryArrows = (props: ExtendedReferenceDotProps) => { const SwapTokenChart = () => { const { t } = useTranslation('common') - const inputBank = mangoStore((s) => s.swap.inputBank) - const outputBank = mangoStore((s) => s.swap.outputBank) + const { inputBank, outputBank, flipPrices } = mangoStore((s) => s.swap) const { inputCoingeckoId, outputCoingeckoId } = useJupiterSwapData() const [baseTokenId, setBaseTokenId] = useState(inputCoingeckoId) const [quoteTokenId, setQuoteTokenId] = useState(outputCoingeckoId) @@ -237,10 +189,6 @@ const SwapTokenChart = () => { ANIMATION_SETTINGS_KEY, INITIAL_ANIMATION_SETTINGS, ) - const [swapChartSettings, setSwapChartSettings] = useLocalStorageState( - SWAP_CHART_SETTINGS_KEY, - [], - ) const swapHistory = mangoStore((s) => s.mangoAccount.swapHistory.data) const loadSwapHistory = mangoStore((s) => s.mangoAccount.swapHistory.loading) const [showSwaps, setShowSwaps] = useState(true) @@ -255,19 +203,6 @@ const SwapTokenChart = () => { return [inputBank.name, outputBank.name] }, [inputBank, outputBank]) - 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 swapMarketName = useMemo(() => { if (!inputBankName || !outputBankName) return '' const inputSymbol = formatTokenSymbol(inputBankName) @@ -397,11 +332,19 @@ const SwapTokenChart = () => { isFetching, } = useQuery( ['swap-chart-data', baseTokenId, quoteTokenId, daysToShow, flipPrices], - () => fetchChartData(baseTokenId, quoteTokenId, daysToShow, flipPrices), + () => + fetchChartData( + baseTokenId, + inputBank, + quoteTokenId, + outputBank, + daysToShow, + flipPrices, + ), { cacheTime: 1000 * 60 * 15, staleTime: 1000 * 60 * 1, - enabled: !!baseTokenId && !!quoteTokenId, + enabled: !!(baseTokenId && quoteTokenId), refetchOnWindowFocus: false, }, ) @@ -454,22 +397,6 @@ const SwapTokenChart = () => { }) }, [coingeckoData, chartSwapTimes]) - const latestChartDataItem = useMemo(() => { - if (!inputBank || !outputBank) return [] - const price = flipPrices - ? outputBank.uiPrice / inputBank.uiPrice - : inputBank.uiPrice / outputBank.uiPrice - const item: ChartDataItem[] = [ - { - price, - time: Date.now(), - inputTokenPrice: inputBank.uiPrice, - outputTokenPrice: outputBank.uiPrice, - }, - ] - return item - }, [flipPrices, inputBank, outputBank]) - const chartData = useMemo(() => { if (!coingeckoData || !coingeckoData.length || coingeckoData.length < 2) return [] @@ -479,11 +406,8 @@ const SwapTokenChart = () => { const swapPoints = swapHistoryPoints.filter( (point) => point.time >= minTime && point.time <= maxTime, ) - return coingeckoData - .concat(swapPoints) - .sort((a, b) => a.time - b.time) - .concat(latestChartDataItem) - } else return coingeckoData.concat(latestChartDataItem) + return coingeckoData.concat(swapPoints).sort((a, b) => a.time - b.time) + } else return coingeckoData }, [coingeckoData, swapHistoryPoints, showSwaps]) const handleMouseMove: CategoricalChartFunc = useCallback( @@ -553,13 +477,9 @@ const SwapTokenChart = () => { - handleFlipPrices( - !flipPrices, - inputBankName, - outputBankName, - swapChartSettings, - setSwapChartSettings, - ) + set((state) => { + state.swap.flipPrices = !flipPrices + }) } hideBg > diff --git a/store/mangoStore.ts b/store/mangoStore.ts index 83abcb1a..f32f5ef9 100644 --- a/store/mangoStore.ts +++ b/store/mangoStore.ts @@ -219,7 +219,7 @@ export type MangoStore = { swapMode: 'ExactIn' | 'ExactOut' amountIn: string amountOut: string - limitPrice?: string + flipPrices: boolean } set: (x: (x: MangoStore) => void) => void themeData: ThemeData @@ -386,7 +386,7 @@ const mangoStore = create()( swapMode: 'ExactIn', amountIn: '', amountOut: '', - limitPrice: '', + flipPrices: false, }, themeData: nftThemeMeta.default, tokenStats: { diff --git a/types/index.ts b/types/index.ts index e6e021a7..2a865366 100644 --- a/types/index.ts +++ b/types/index.ts @@ -486,9 +486,3 @@ export interface ContributionDetails { perpMarketContributions: PerpMarketContribution[] spotUi: number } - -export type SwapChartSettings = { - pair: string - base: string - quote: string -} diff --git a/utils/constants.ts b/utils/constants.ts index c7c5f5d6..4ed66439 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -59,8 +59,6 @@ export const SWAP_MARGIN_KEY = 'swapMargin-0.1' export const SHOW_SWAP_INTRO_MODAL = 'showSwapModal-0.1' -export const SWAP_CHART_SETTINGS_KEY = 'swapChartSettings-0.1' - export const ACCEPT_TERMS_KEY = 'termsOfUseAccepted-0.1' export const TRADE_LAYOUT_KEY = 'tradeLayoutKey-0.1' From 70f9d6da1623e865d365f187514ba634f30634ff Mon Sep 17 00:00:00 2001 From: saml33 Date: Thu, 17 Aug 2023 13:57:39 +1000 Subject: [PATCH 44/44] add mobile swap orders table --- components/swap/SwapOrders.tsx | 430 ++++++++++++++++++++++---------- public/locales/en/trade.json | 1 + public/locales/es/trade.json | 1 + public/locales/ru/trade.json | 1 + public/locales/zh/trade.json | 7 +- public/locales/zh_tw/trade.json | 1 + 6 files changed, 300 insertions(+), 141 deletions(-) diff --git a/components/swap/SwapOrders.tsx b/components/swap/SwapOrders.tsx index 3b63aba6..07748173 100644 --- a/components/swap/SwapOrders.tsx +++ b/components/swap/SwapOrders.tsx @@ -8,25 +8,34 @@ import { TrBody, TrHead, } from '@components/shared/TableElements' -import { NoSymbolIcon, TrashIcon } from '@heroicons/react/20/solid' +import { + ChevronDownIcon, + NoSymbolIcon, + TrashIcon, +} from '@heroicons/react/20/solid' import { BN } from '@project-serum/anchor' import { useWallet } from '@solana/wallet-adapter-react' import mangoStore from '@store/mangoStore' import useMangoAccount from 'hooks/useMangoAccount' import useMangoGroup from 'hooks/useMangoGroup' import { useSortableData } from 'hooks/useSortableData' +import { useViewport } from 'hooks/useViewport' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { notify } from 'utils/notifications' import { floorToDecimal } from 'utils/numbers' +import { breakpoints } from 'utils/theme' import * as sentry from '@sentry/nextjs' import { isMangoError } from 'types' import Loading from '@components/shared/Loading' import SideBadge from '@components/shared/SideBadge' +import { Disclosure, Transition } from '@headlessui/react' +import SheenLoader from '@components/shared/SheenLoader' -// todo: add mobile table const SwapOrders = () => { const { t } = useTranslation(['common', 'swap', 'trade']) + const { width } = useViewport() + const showTableView = width ? width > breakpoints.md : false const { mangoAccount, mangoAccountAddress } = useMangoAccount() const { group } = useMangoGroup() const { connected } = useWallet() @@ -174,87 +183,171 @@ const SwapOrders = () => { } return orders.length ? ( - - - - - - - - - - - - - + + {({ open }) => ( + <> + +
+
+ {pair} + +

+ {size} + + {' '} + {bank.name} + + + {' at '} + + {triggerPrice} + + {' '} + {side === 'buy' ? sellBank.name : buyBank.name} + +

+
+ +
+
+ + +
+
+

+ {t('trade:size')} +

+

+ {size} + + {' '} + {bank.name} + +

+
+
+

+ {t('trade:filled')} +

+

+ {filled}/{size} + + {' '} + {bank.name} + +

+
+
+

+ {t('trade:current-price')} +

+

+ {currentPrice} + + {' '} + {buyBank.name} + +

+
+
+

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

+

+ {triggerPrice} + + {' '} + {side === 'buy' ? sellBank.name : buyBank.name} + +

+
+
+

+ {t('trade:est-slippage')} +

+

+ {fee.toFixed(2)}% +

+
+ +
+

{t('cancel')}

+ handleCancel(data.id)}> + {cancelId === data.id.toString() ? ( + +
+ + ) : ( + t('trade:cancel-order') + )} + +
+
+ + + + )} + ) })} - -
- requestSort('pair')} - sortConfig={sortConfig} - title={t('swap:pair')} - /> - -
+ showTableView ? ( + + + + - - - - - - - - - + + + + + + + + + + + + {tableData.map((data, i) => { + const { + buyBank, + currentPrice, + fee, + pair, + sellBank, + side, + size, + filled, + triggerPrice, + } = data + + const bank = side === 'buy' ? buyBank : sellBank + return ( + + + + + + + + + + + ) + })} + +
requestSort('side')} + sortKey="pair" + sort={() => requestSort('pair')} sortConfig={sortConfig} - title={t('trade:side')} + title={t('swap:pair')} /> - - -
- requestSort('size')} - sortConfig={sortConfig} - title={t('trade:size')} - /> -
-
-
- requestSort('filled')} - sortConfig={sortConfig} - title={t('trade:filled')} - /> -
-
-
- requestSort('currentPrice')} - sortConfig={sortConfig} - title={t('trade:current-price')} - /> -
-
-
- requestSort('triggerPrice')} - sortConfig={sortConfig} - title={t('trade:trigger-price')} - /> -
-
-
- requestSort('fee')} - sortConfig={sortConfig} - title={t('trade:est-slippage')} - /> -
-
-
- - {t('trade:cancel-all')} - -
-
+
+ requestSort('side')} + sortConfig={sortConfig} + title={t('trade:side')} + /> +
+
+
+ requestSort('size')} + sortConfig={sortConfig} + title={t('trade:size')} + /> +
+
+
+ requestSort('filled')} + sortConfig={sortConfig} + title={t('trade:filled')} + /> +
+
+
+ requestSort('currentPrice')} + sortConfig={sortConfig} + title={t('trade:current-price')} + /> +
+
+
+ requestSort('triggerPrice')} + sortConfig={sortConfig} + title={t('trade:trigger-price')} + /> +
+
+
+ requestSort('fee')} + sortConfig={sortConfig} + title={t('trade:est-slippage')} + /> +
+
+
+ + {t('trade:cancel-all')} + +
+
{pair} +
+ +
+
+

+ {size} + + {' '} + {bank.name} + +

+
+

+ {filled}/{size} + + {' '} + {bank.name} + +

+
+

+ {currentPrice} + + {' '} + {buyBank.name} + +

+
+

+ {triggerPrice} + + {' '} + {side === 'buy' ? sellBank.name : buyBank.name} + +

+
+

{fee.toFixed(2)}%

+
+ handleCancel(data.id)} + size="small" + > + {cancelId === data.id.toString() || cancelId === 'all' ? ( + + ) : ( + + )} + +
+ ) : ( +
{tableData.map((data, i) => { const { buyBank, @@ -270,66 +363,127 @@ const SwapOrders = () => { const bank = side === 'buy' ? buyBank : sellBank return ( - -
{pair} -
- -
-
-

- {size} - {bank.name} -

-
-

- {filled}/{size} - {bank.name} -

-
-

- {currentPrice} - - {' '} - {buyBank.name} - -

-
-

- {triggerPrice} - - {' '} - {side === 'buy' ? sellBank.name : buyBank.name} - -

-
-

{fee.toFixed(2)}%

-
- handleCancel(data.id)} - size="small" - > - {cancelId === data.id.toString() || cancelId === 'all' ? ( - - ) : ( - - )} - -
+
+ ) ) : mangoAccountAddress || connected ? (
diff --git a/public/locales/en/trade.json b/public/locales/en/trade.json index 7430648b..5f6f74db 100644 --- a/public/locales/en/trade.json +++ b/public/locales/en/trade.json @@ -9,6 +9,7 @@ "book": "Book", "buys": "Buys", "cancel-all": "Cancel All", + "cancel-order": "Cancel Order", "cancel-order-error": "Failed to cancel order", "close-confirm": "Market close your {{config_name}} position", "close-position": "Close Position", diff --git a/public/locales/es/trade.json b/public/locales/es/trade.json index 7430648b..5f6f74db 100644 --- a/public/locales/es/trade.json +++ b/public/locales/es/trade.json @@ -9,6 +9,7 @@ "book": "Book", "buys": "Buys", "cancel-all": "Cancel All", + "cancel-order": "Cancel Order", "cancel-order-error": "Failed to cancel order", "close-confirm": "Market close your {{config_name}} position", "close-position": "Close Position", diff --git a/public/locales/ru/trade.json b/public/locales/ru/trade.json index 7430648b..5f6f74db 100644 --- a/public/locales/ru/trade.json +++ b/public/locales/ru/trade.json @@ -9,6 +9,7 @@ "book": "Book", "buys": "Buys", "cancel-all": "Cancel All", + "cancel-order": "Cancel Order", "cancel-order-error": "Failed to cancel order", "close-confirm": "Market close your {{config_name}} position", "close-position": "Close Position", diff --git a/public/locales/zh/trade.json b/public/locales/zh/trade.json index c7cedf2c..6630a69e 100644 --- a/public/locales/zh/trade.json +++ b/public/locales/zh/trade.json @@ -5,12 +5,11 @@ "average-funding": "平均 {{interval}} 资金费", "avg-entry-price": "平均开仓价格", "average-price-of": "at an average price of", - "cancel-all": "Cancel All", - "falls-to": "falls to", - "filled": "Filled", "base": "基础", "book": "单薄", "buys": "买", + "cancel-all": "Cancel All", + "cancel-order": "Cancel Order", "cancel-order-error": "取消订单失败", "close-confirm": "市场平仓 {{config_name}}", "close-position": "平仓", @@ -24,6 +23,8 @@ "edit-order": "编辑订单", "est-liq-price": "预计清算价格", "est-slippage": "预计下滑", + "falls-to": "falls to", + "filled": "Filled", "for": "for", "funding-limits": "资金费限制", "funding-rate": "1小时平均资金费", diff --git a/public/locales/zh_tw/trade.json b/public/locales/zh_tw/trade.json index a80a004f..91979b2c 100644 --- a/public/locales/zh_tw/trade.json +++ b/public/locales/zh_tw/trade.json @@ -9,6 +9,7 @@ "book": "單薄", "buys": "買", "cancel-all": "Cancel All", + "cancel-order": "Cancel Order", "cancel-order-error": "取消訂單失敗", "close-confirm": "市場平倉 {{config_name}}", "close-position": "平倉",