From e478d6405a3b45004fec018e1cc6f5f23094caf7 Mon Sep 17 00:00:00 2001 From: tjs Date: Sun, 16 Jul 2023 23:41:13 -0400 Subject: [PATCH] use jupiter on trade page --- components/settings/RpcSettings.tsx | 12 +- components/swap/SwapForm.tsx | 63 ++- components/swap/SwapReviewRouteInfo.tsx | 2 +- components/swap/SwapSlider.tsx | 7 +- components/swap/useQuoteRoutes.ts | 31 +- components/trade/AdvancedTradeForm.tsx | 557 ++++++++++--------- components/trade/SpotMarketOrderSwapForm.tsx | 409 ++++++++++++++ public/locales/en/common.json | 1 + store/mangoStore.ts | 13 +- utils/constants.ts | 2 +- utils/numbers.ts | 8 + 11 files changed, 777 insertions(+), 328 deletions(-) create mode 100644 components/trade/SpotMarketOrderSwapForm.tsx diff --git a/components/settings/RpcSettings.tsx b/components/settings/RpcSettings.tsx index 78fcfb01..8160321e 100644 --- a/components/settings/RpcSettings.tsx +++ b/components/settings/RpcSettings.tsx @@ -14,18 +14,18 @@ import { } from 'utils/constants' import Tooltip from '@components/shared/Tooltip' +export const TRITON_DEDICATED_URL = process.env.NEXT_PUBLIC_TRITON_TOKEN + ? `https://mango.rpcpool.com/${process.env.NEXT_PUBLIC_TRITON_TOKEN}` + : 'https://mango.rpcpool.com/946ef7337da3f5b8d3e4a34e7f88' + const RPC_URLS = [ { label: 'Triton Shared', - value: - process.env.NEXT_PUBLIC_ENDPOINT || - 'https://mango.rpcpool.com/946ef7337da3f5b8d3e4a34e7f88', + value: process.env.NEXT_PUBLIC_ENDPOINT || TRITON_DEDICATED_URL, }, { label: 'Triton Dedicated', - value: process.env.NEXT_PUBLIC_TRITON_TOKEN - ? `https://mango.rpcpool.com/${process.env.NEXT_PUBLIC_TRITON_TOKEN}` - : 'https://mango.rpcpool.com/946ef7337da3f5b8d3e4a34e7f88', + value: TRITON_DEDICATED_URL, }, // { // label: 'Genesys Go', diff --git a/components/swap/SwapForm.tsx b/components/swap/SwapForm.tsx index 4665cb8d..39ab5c4f 100644 --- a/components/swap/SwapForm.tsx +++ b/components/swap/SwapForm.tsx @@ -15,7 +15,6 @@ 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' import { Transition } from '@headlessui/react' @@ -85,27 +84,25 @@ const SwapForm = () => { 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) + 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 { bestRoute, routes } = useQuoteRoutes({ inputMint: inputBank?.mint.toString() || USDC_MINT, outputMint: outputBank?.mint.toString() || MANGO_MINT, - amount: swapMode === 'ExactIn' ? debouncedAmountIn : debouncedAmountOut, + amount: swapMode === 'ExactIn' ? amountInFormValue : amountOutFormValue, slippage, swapMode, wallet: publicKey?.toBase58(), @@ -127,6 +124,13 @@ const SwapForm = () => { [] ) + const setAmountFromSlider = useCallback( + (amount: string) => { + setAmountInFormValue(amount, true) + }, + [setAmountInFormValue] + ) + const setAmountOutFormValue = useCallback((amountOut: string) => { set((s) => { s.swap.amountOut = amountOut @@ -214,20 +218,23 @@ 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 handleSwitchTokens = useCallback( + (amountIn: Decimal, amountOut: Decimal) => { + if (amountIn?.gt(0) && amountOut.gte(0)) { + setAmountInFormValue(amountOut.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] + ) const maintProjectedHealth = useMemo(() => { const group = mangoStore.getState().group @@ -385,7 +392,9 @@ const SwapForm = () => {
-
handleSubmit(e)}> -
- {tradeForm.tradeType === 'Limit' ? ( - <> -
-

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

-
-
- {quoteLogoURI ? ( -
- + {tradeForm.tradeType === 'Market' && + selectedMarket instanceof Serum3Market ? ( + <> + + + ) : ( + <> + handleSubmit(e)}> +
+ {tradeForm.tradeType === 'Limit' ? ( + <> +
+

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

+
+ {quoteLogoURI ? ( +
+ +
+ ) : ( +
+ +
+ )} + +
{quoteSymbol}
+
+ + ) : null} + +
+
+ +
+ + } + /> +
+
+ {baseSymbol} +
+
+
+ {quoteLogoURI ? ( +
+ +
+ ) : ( +
+ +
+ )} + +
{quoteSymbol}
+
+ {minOrderSize && + tradeForm.baseSize && + parseFloat(tradeForm.baseSize) < minOrderSize ? ( +
+ +
+ ) : null} +
+
+
+ {selectedMarket instanceof Serum3Market ? ( + tradeFormSizeUi === 'slider' ? ( + ) : ( -
- -
- )} - + ) + ) : tradeFormSizeUi === 'slider' ? ( + -
{quoteSymbol}
-
- - ) : null} - + )} +
+
+ {tradeForm.tradeType === 'Limit' ? ( +
+
+ + handlePostOnlyChange(e.target.checked)} + > + {t('trade:post')} + + +
+
+ +
+ handleIocChange(e.target.checked)} + > + IOC + +
+
+
+
+ ) : null} + {selectedMarket instanceof Serum3Market ? ( +
+ + + {t('trade:margin')} + + +
+ ) : ( +
+ +
+ + handleReduceOnlyChange(e.target.checked) + } + > + {t('trade:reduce-only')} + +
+
+
+ )} +
+
+ {ipAllowed ? ( + + ) : ( + + )} +
+ + -
-
- -
- - } - /> -
-
- {baseSymbol} -
-
-
- {quoteLogoURI ? ( -
- -
- ) : ( -
- -
- )} - -
{quoteSymbol}
-
- {minOrderSize && - tradeForm.baseSize && - parseFloat(tradeForm.baseSize) < minOrderSize ? ( -
- -
- ) : null} -
-
-
- {selectedMarket instanceof Serum3Market ? ( - tradeFormSizeUi === 'slider' ? ( - - ) : ( - - ) - ) : tradeFormSizeUi === 'slider' ? ( - - ) : ( - - )} -
-
- {tradeForm.tradeType === 'Limit' ? ( -
-
- - handlePostOnlyChange(e.target.checked)} - > - {t('trade:post')} - - -
-
- -
- handleIocChange(e.target.checked)} - > - IOC - -
-
-
-
- ) : null} - {selectedMarket instanceof Serum3Market ? ( -
- - - {t('trade:margin')} - - -
- ) : ( -
- -
- handleReduceOnlyChange(e.target.checked)} - > - {t('trade:reduce-only')} - -
-
-
- )} -
-
- {ipAllowed ? ( - - ) : ( - - )} -
- - {tradeForm.tradeType === 'Market' ? ( -
- -
- ) : null} - + + )}
) } diff --git a/components/trade/SpotMarketOrderSwapForm.tsx b/components/trade/SpotMarketOrderSwapForm.tsx new file mode 100644 index 00000000..c7339ce2 --- /dev/null +++ b/components/trade/SpotMarketOrderSwapForm.tsx @@ -0,0 +1,409 @@ +import mangoStore from '@store/mangoStore' +import NumberFormat, { + NumberFormatValues, + SourceInfo, +} from 'react-number-format' +import { + INPUT_PREFIX_CLASSNAMES, + INPUT_SUFFIX_CLASSNAMES, +} from './AdvancedTradeForm' +import LogoWithFallback from '@components/shared/LogoWithFallback' +import { LinkIcon, QuestionMarkCircleIcon } from '@heroicons/react/20/solid' +import useSelectedMarket from 'hooks/useSelectedMarket' +import { useWallet } from '@solana/wallet-adapter-react' +import useIpAddress from 'hooks/useIpAddress' +import { useTranslation } from 'next-i18next' +import { FormEvent, useCallback, useMemo, useState } from 'react' +import Loading from '@components/shared/Loading' +import Button from '@components/shared/Button' +import Image from 'next/image' +import useQuoteRoutes from '@components/swap/useQuoteRoutes' +import { + Serum3Market, + fetchJupiterTransaction, +} from '@blockworks-foundation/mango-v4' +import Decimal from 'decimal.js' +import { useEnhancedWallet } from '@components/wallet/EnhancedWalletProvider' +import { notify } from 'utils/notifications' +import * as sentry from '@sentry/nextjs' +import { isMangoError } from 'types' +import SwapSlider from '@components/swap/SwapSlider' +import PercentageSelectButtons from '@components/swap/PercentageSelectButtons' +import { SIZE_INPUT_UI_KEY } from 'utils/constants' +import useLocalStorageState from 'hooks/useLocalStorageState' + +const set = mangoStore.getState().set +const slippage = 100 + +function stringToNumberOrZero(s: string): number { + const n = parseFloat(s) + if (isNaN(n)) { + return 0 + } + return n +} + +export default function SpotMarketOrderSwapForm() { + const { t } = useTranslation(['common', 'trade']) + const { baseSize, price, quoteSize, side } = mangoStore((s) => s.tradeForm) + const [placingOrder, setPlacingOrder] = useState(false) + const { ipAllowed, ipCountry } = useIpAddress() + const { connected, publicKey } = useWallet() + const { handleConnect } = useEnhancedWallet() + const [swapFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider') + const { + selectedMarket, + price: oraclePrice, + baseLogoURI, + baseSymbol, + quoteLogoURI, + quoteSymbol, + serumOrPerpMarket, + } = useSelectedMarket() + + const handleBaseSizeChange = useCallback( + (e: NumberFormatValues, info: SourceInfo) => { + if (info.source !== 'event') return + console.log('base size change') + + set((s) => { + const price = + s.tradeForm.tradeType === 'Market' + ? oraclePrice + : Number(s.tradeForm.price) + + s.tradeForm.baseSize = e.value + if (price && e.value !== '' && !Number.isNaN(Number(e.value))) { + s.tradeForm.quoteSize = new Decimal(price).mul(e.value).toFixed() + } else { + s.tradeForm.quoteSize = '' + } + }) + }, + [oraclePrice] + ) + + const handleQuoteSizeChange = useCallback( + (e: NumberFormatValues, info: SourceInfo) => { + if (info.source !== 'event') return + set((s) => { + const price = + s.tradeForm.tradeType === 'Market' + ? oraclePrice + : Number(s.tradeForm.price) + + s.tradeForm.quoteSize = e.value + if (price && e.value !== '' && !Number.isNaN(Number(e.value))) { + s.tradeForm.baseSize = new Decimal(e.value).div(price).toFixed() + } else { + s.tradeForm.baseSize = '' + } + }) + }, + [oraclePrice] + ) + + console.log('side outer', side) + + const setAmountFromSlider = useCallback( + (amount: string) => { + console.log('amount', amount) + + if (side === 'buy') { + handleQuoteSizeChange( + { value: amount } as NumberFormatValues, + { source: 'event' } as SourceInfo + ) + } else { + handleBaseSizeChange( + { value: amount } as NumberFormatValues, + { source: 'event' } as SourceInfo + ) + } + }, + [side] + ) + + const [inputBank, outputBank] = useMemo(() => { + const group = mangoStore.getState().group + if (!group || !(selectedMarket instanceof Serum3Market)) return [] + + const quoteBank = group?.getFirstBankByTokenIndex( + selectedMarket.quoteTokenIndex + ) + const baseBank = group.getFirstBankByTokenIndex( + selectedMarket.baseTokenIndex + ) + + if (side === 'buy') { + set((s) => { + s.swap.inputBank = quoteBank + s.swap.outputBank = baseBank + }) + return [quoteBank, baseBank] + } else { + set((s) => { + s.swap.inputBank = baseBank + s.swap.outputBank = quoteBank + }) + return [baseBank, quoteBank] + } + }, [selectedMarket, side]) + + const { bestRoute: selectedRoute, isLoading } = useQuoteRoutes({ + inputMint: inputBank?.mint.toString() || '', + outputMint: outputBank?.mint.toString() || '', + amount: side === 'buy' ? quoteSize : baseSize, + slippage, + swapMode: 'ExactIn', + wallet: publicKey?.toBase58(), + }) + + const handlePlaceOrder = useCallback(async () => { + const client = mangoStore.getState().client + const group = mangoStore.getState().group + const mangoAccount = mangoStore.getState().mangoAccount.current + const { baseSize, quoteSize, side } = mangoStore.getState().tradeForm + const actions = mangoStore.getState().actions + const connection = mangoStore.getState().connection + + if (!group || !mangoAccount) return + console.log('placing order') + + if ( + !mangoAccount || + !group || + !inputBank || + !outputBank || + !publicKey || + !selectedRoute + ) { + console.log( + mangoAccount, + group, + inputBank, + outputBank, + publicKey, + selectedRoute + ) + return + } + + setPlacingOrder(true) + console.log('placing order 2') + console.log('selected route', selectedRoute) + + const [ixs, alts] = await fetchJupiterTransaction( + connection, + selectedRoute, + publicKey, + slippage, + inputBank.mint, + outputBank.mint + ) + + try { + const tx = await client.marginTrade({ + group, + mangoAccount, + inputMintPk: inputBank.mint, + amountIn: + side === 'buy' + ? stringToNumberOrZero(quoteSize) + : stringToNumberOrZero(baseSize), + outputMintPk: outputBank.mint, + userDefinedInstructions: ixs, + userDefinedAlts: alts, + flashLoanType: { swap: {} }, + }) + set((s) => { + s.successAnimation.swap = true + }) + // if (soundSettings['swap-success']) { + // successSound.play() + // } + notify({ + title: 'Transaction confirmed', + type: 'success', + txid: tx, + noSound: true, + }) + actions.fetchGroup() + actions.fetchSwapHistory(mangoAccount.publicKey.toString(), 30000) + await actions.reloadMangoAccount() + set((s) => { + s.tradeForm.baseSize = '' + s.tradeForm.quoteSize = '' + }) + } 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', + }) + } + } finally { + setPlacingOrder(false) + } + }, [inputBank, outputBank, publicKey, selectedRoute]) + + const handleSubmit = (e: FormEvent) => { + e.preventDefault() + connected ? handlePlaceOrder() : handleConnect() + } + + const disabled = + (connected && (!baseSize || !price)) || + !serumOrPerpMarket || + parseFloat(baseSize) < serumOrPerpMarket.minOrderSize || + isLoading + + return ( + <> +
handleSubmit(e)}> +
+
+
+ +
+ + } + /> +
+
+ {baseSymbol} +
+
+
+ {quoteLogoURI ? ( +
+ +
+ ) : ( +
+ +
+ )} + +
{quoteSymbol}
+
+
+
+
+ {swapFormSizeUi === 'slider' ? ( + setAmountFromSlider(v)} + step={1 / 10 ** (inputBank?.mintDecimals || 6)} + /> + ) : ( + setAmountFromSlider(v)} + useMargin={true} + /> + )} +
+
+ {ipAllowed ? ( + + ) : ( + + )} +
+
+ + ) +} diff --git a/public/locales/en/common.json b/public/locales/en/common.json index c0b0e369..3b10e28d 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -76,6 +76,7 @@ "explorer": "Explorer", "fee": "Fee", "fees": "Fees", + "fetching-route": "Finding Route", "free-collateral": "Free Collateral", "get-started": "Get Started", "governance": "Governance", diff --git a/store/mangoStore.ts b/store/mangoStore.ts index 6e92066c..087fabab 100644 --- a/store/mangoStore.ts +++ b/store/mangoStore.ts @@ -65,7 +65,10 @@ import { import spotBalancesUpdater from './spotBalancesUpdater' import { PerpMarket } from '@blockworks-foundation/mango-v4/' import perpPositionsUpdater from './perpPositionsUpdater' -import { DEFAULT_PRIORITY_FEE } from '@components/settings/RpcSettings' +import { + DEFAULT_PRIORITY_FEE, + TRITON_DEDICATED_URL, +} from '@components/settings/RpcSettings' import { IExecutionLineAdapter, IOrderLineAdapter, @@ -76,12 +79,8 @@ const GROUP = new PublicKey('78b8f4cGCwmZ9ysPFMWLaLTkkaYnUjwMJYStWe5RTSSX') const ENDPOINTS = [ { name: 'mainnet-beta', - url: - process.env.NEXT_PUBLIC_ENDPOINT || - 'https://mango.rpcpool.com/946ef7337da3f5b8d3e4a34e7f88', - websocket: - process.env.NEXT_PUBLIC_ENDPOINT || - 'https://mango.rpcpool.com/946ef7337da3f5b8d3e4a34e7f88', + url: process.env.NEXT_PUBLIC_ENDPOINT || TRITON_DEDICATED_URL, + websocket: process.env.NEXT_PUBLIC_ENDPOINT || TRITON_DEDICATED_URL, custom: false, }, { diff --git a/utils/constants.ts b/utils/constants.ts index 4b075ee2..b6074522 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -43,7 +43,7 @@ export const FAVORITE_MARKETS_KEY = 'favoriteMarkets-0.2' export const THEME_KEY = 'theme-0.1' -export const RPC_PROVIDER_KEY = 'rpcProviderKey-0.6' +export const RPC_PROVIDER_KEY = 'rpcProviderKey-0.7' export const PRIORITY_FEE_KEY = 'priorityFeeKey-0.1' diff --git a/utils/numbers.ts b/utils/numbers.ts index 8afc54b7..7285d5bf 100644 --- a/utils/numbers.ts +++ b/utils/numbers.ts @@ -150,3 +150,11 @@ export const numberCompacter = Intl.NumberFormat('en', { maximumFractionDigits: 2, notation: 'compact', }) + +export function stringToNumber(s: string): number | undefined { + const n = parseFloat(s) + if (isNaN(n)) { + return undefined + } + return n +}