From d4d38aef15b3a142bc6bd99107c3cf0510f3620c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Brzezi=C5=84ski?= Date: Mon, 20 May 2024 01:57:10 +0200 Subject: [PATCH] refactor swaps routing function + add more fallback orcales (#426) * cleanup + overloads for routing functions, new types for routing functions * add new routing route * add fallback oracles * yarn deduplicate --- components/governance/ListToken/ListToken.tsx | 2 - components/swap/MarketSwapForm.tsx | 36 +- components/swap/WalletSwapForm.tsx | 3 +- components/swap/useQuoteRoutes.ts | 447 +++++++++++------- components/trade/SpotMarketOrderSwapForm.tsx | 3 +- store/mangoStore.ts | 4 + utils/constants.ts | 2 +- utils/swap/raydium.ts | 1 + 8 files changed, 295 insertions(+), 203 deletions(-) diff --git a/components/governance/ListToken/ListToken.tsx b/components/governance/ListToken/ListToken.tsx index cc83f6ae..38e04758 100644 --- a/components/governance/ListToken/ListToken.tsx +++ b/components/governance/ListToken/ListToken.tsx @@ -344,8 +344,6 @@ const ListToken = ({ goBack }: { goBack: () => void }) => { undefined, // mangoAccount onlyDirect ? 'JUPITER_DIRECT' : 'JUPITER', connection, - null, - false, ) }, [wallet.publicKey, connection], diff --git a/components/swap/MarketSwapForm.tsx b/components/swap/MarketSwapForm.tsx index f16bfd64..e550cf27 100644 --- a/components/swap/MarketSwapForm.tsx +++ b/components/swap/MarketSwapForm.tsx @@ -83,26 +83,7 @@ const MarketSwapForm = ({ swapMode, wallet: publicKey?.toBase58(), mangoAccount, - mangoAccountSwap: true, - enabled: () => - !!( - inputBank?.mint && - outputBank?.mint && - quoteAmount && - !isDraggingSlider - ), - }) - - const { bestRoute: bestDirectRoute } = useQuoteRoutes({ - inputMint: inputBank?.mint.toString(), - outputMint: outputBank?.mint.toString(), - amount: quoteAmount, - slippage, - swapMode, - wallet: publicKey?.toBase58(), - mangoAccount, - mangoAccountSwap: true, - mode: 'JUPITER_DIRECT', + routingMode: 'ALL_AND_JUPITER_DIRECT', enabled: () => !!( inputBank?.mint && @@ -233,11 +214,8 @@ const MarketSwapForm = ({ depending on the swapMode and set those values in state */ useEffect(() => { - if ( - typeof bestRoute !== 'undefined' || - typeof bestDirectRoute !== 'undefined' - ) { - const newRoute = bestRoute || bestDirectRoute + if (typeof bestRoute !== 'undefined') { + const newRoute = bestRoute setSelectedRoute(newRoute) if (inputBank && swapMode === 'ExactOut' && newRoute?.inAmount) { @@ -252,7 +230,7 @@ const MarketSwapForm = ({ setAmountOutFormValue(outAmount) } } - }, [bestRoute, bestDirectRoute, swapMode, inputBank, outputBank]) + }, [bestRoute, swapMode, inputBank, outputBank]) const handleSwitchTokens = useCallback(() => { if (amountInAsDecimal?.gt(0) && amountOutAsDecimal.gte(0)) { @@ -318,10 +296,8 @@ const MarketSwapForm = ({ onSuccess={onSuccess} refetchRoute={refetchRoute} routes={ - bestRoute || bestDirectRoute - ? ([bestRoute, bestDirectRoute].filter( - (x) => x && !x.error, - ) as JupiterV6RouteInfo[]) + bestRoute + ? ([bestRoute].filter((x) => x && !x.error) as JupiterV6RouteInfo[]) : undefined } selectedRoute={selectedRoute} diff --git a/components/swap/WalletSwapForm.tsx b/components/swap/WalletSwapForm.tsx index 3c5f6b6f..692b7b1e 100644 --- a/components/swap/WalletSwapForm.tsx +++ b/components/swap/WalletSwapForm.tsx @@ -69,8 +69,7 @@ const WalletSwapForm = ({ setShowTokenSelect }: WalletSwapFormProps) => { swapMode, wallet: publicKey?.toBase58(), mangoAccount: undefined, - mangoAccountSwap: false, - mode: 'JUPITER', + routingMode: 'JUPITER', enabled: () => !!( inputBank?.mint && diff --git a/components/swap/useQuoteRoutes.ts b/components/swap/useQuoteRoutes.ts index 839ace76..05d80313 100644 --- a/components/swap/useQuoteRoutes.ts +++ b/components/swap/useQuoteRoutes.ts @@ -3,7 +3,7 @@ import { Connection, PublicKey } from '@solana/web3.js' import { useQuery } from '@tanstack/react-query' import Decimal from 'decimal.js' import { JupiterV6RouteInfo } from 'types/jupiter' -// import { MANGO_ROUTER_API_URL } from 'utils/constants' +import { MANGO_ROUTER_API_URL } from 'utils/constants' import useJupiterSwapData from './useJupiterSwapData' import { useMemo } from 'react' import { JUPITER_V6_QUOTE_API_MAINNET } from 'utils/constants' @@ -12,79 +12,110 @@ import { findRaydiumPoolInfo, getSwapTransaction } from 'utils/swap/raydium' import mangoStore from '@store/mangoStore' import { fetchJupiterTransaction } from './SwapReviewRouteInfo' -type SwapModes = 'ALL' | 'JUPITER' | 'MANGO' | 'JUPITER_DIRECT' | 'RAYDIUM' +type SwapModes = 'ExactIn' | 'ExactOut' + +type MultiRoutingMode = 'ALL' | 'ALL_AND_JUPITER_DIRECT' + +type JupiterRoutingMode = 'JUPITER_DIRECT' | 'JUPITER' + +type RaydiumRoutingMode = 'RAYDIUM' + +type MangoRoutingMode = 'MANGO' + +type RoutingMode = + | MultiRoutingMode + | JupiterRoutingMode + | RaydiumRoutingMode + | MangoRoutingMode type useQuoteRoutesPropTypes = { inputMint: string | undefined outputMint: string | undefined amount: string slippage: number - swapMode: string + swapMode: SwapModes wallet: string | undefined mangoAccount: MangoAccount | undefined - mode?: SwapModes - mangoAccountSwap: boolean + routingMode: RoutingMode enabled?: () => boolean } +function isMultiRoutingMode(value: RoutingMode): value is MultiRoutingMode { + return ['ALL', 'ALL_AND_JUPITER_DIRECT'].includes(value) +} + +function isRaydiumRoutingMode(value: RoutingMode): value is RaydiumRoutingMode { + return value === 'RAYDIUM' +} + const fetchJupiterRoute = async ( inputMint: string | undefined, outputMint: string | undefined, amount = 0, slippage = 50, - swapMode = 'ExactIn', + swapMode: SwapModes = 'ExactIn', onlyDirectRoutes = true, maxAccounts = 64, connection: Connection, wallet: string, ) => { - if (!inputMint || !outputMint) return - try { - { - const paramObj: { - inputMint: string - outputMint: string - amount: string - slippageBps: string - swapMode: string - onlyDirectRoutes: string - maxAccounts?: string - } = { - inputMint: inputMint.toString(), - outputMint: outputMint.toString(), - amount: amount.toString(), - slippageBps: Math.ceil(slippage * 100).toString(), - swapMode, - onlyDirectRoutes: `${onlyDirectRoutes}`, - } - //exact out is not supporting max account - if (swapMode === 'ExactIn') { - paramObj.maxAccounts = maxAccounts.toString() - } - const paramsString = new URLSearchParams(paramObj).toString() - const response = await fetch( - `${JUPITER_V6_QUOTE_API_MAINNET}/quote?${paramsString}`, - ) - const res: JupiterV6RouteInfo = await response.json() - const [ixes] = await fetchJupiterTransaction( - connection, - res, - new PublicKey(wallet), - slippage, - new PublicKey(inputMint), - new PublicKey(outputMint), - ) - return { - bestRoute: - [...ixes.flatMap((x) => x.keys.flatMap((k) => k.pubkey))].length <= + return new Promise<{ bestRoute: JupiterV6RouteInfo }>( + // eslint-disable-next-line no-async-promise-executor + async (resolve, reject) => { + try { + if (!inputMint || !outputMint) return + const paramObj: { + inputMint: string + outputMint: string + amount: string + slippageBps: string + swapMode: string + onlyDirectRoutes: string + maxAccounts?: string + } = { + inputMint: inputMint.toString(), + outputMint: outputMint.toString(), + amount: amount.toString(), + slippageBps: Math.ceil(slippage * 100).toString(), + swapMode, + onlyDirectRoutes: `${onlyDirectRoutes}`, + } + //exact out is not supporting max account + if (swapMode === 'ExactIn') { + paramObj.maxAccounts = maxAccounts.toString() + } + const paramsString = new URLSearchParams(paramObj).toString() + const response = await fetch( + `${JUPITER_V6_QUOTE_API_MAINNET}/quote?${paramsString}`, + ) + const res: JupiterV6RouteInfo = await response.json() + if (res.error) { + throw res.error + } + const [ixes] = await fetchJupiterTransaction( + connection, + res, + new PublicKey(wallet), + slippage, + new PublicKey(inputMint), + new PublicKey(outputMint), + ) + + if ( + [...ixes.flatMap((x) => x.keys.flatMap((k) => k.pubkey))].length > maxAccounts - ? res - : undefined, + ) { + throw 'Max accounts exceeded' + } + resolve({ + bestRoute: res, + }) + } catch (e) { + console.log('jupiter route error:', e) + reject(e) } - } - } catch (e) { - console.log('error fetching jupiter route', e) - } + }, + ) } const fetchRaydiumRoute = async ( @@ -94,102 +125,134 @@ const fetchRaydiumRoute = async ( slippage = 50, connection: Connection, wallet: string, - mangoAccountSwap: boolean, + isInWalletSwap: boolean, ) => { - if (!inputMint || !outputMint) return - try { - const poolKeys = await findRaydiumPoolInfo( - connection, - outputMint, - inputMint, - ) + return new Promise<{ bestRoute: JupiterV6RouteInfo }>( + // eslint-disable-next-line no-async-promise-executor + async (resolve, reject) => { + try { + if (!inputMint || !outputMint) return - if (poolKeys) { - return await getSwapTransaction( - connection, - outputMint, - amount, - poolKeys!, - slippage, - new PublicKey(wallet), - mangoAccountSwap, - ) - } - } catch (e) { - console.log('error fetching raydium route', e) - } + const poolKeys = await findRaydiumPoolInfo( + connection, + outputMint, + inputMint, + ) + + if (poolKeys) { + const resp = await getSwapTransaction( + connection, + outputMint, + amount, + poolKeys!, + slippage, + new PublicKey(wallet), + isInWalletSwap, + ) + resolve(resp as unknown as { bestRoute: JupiterV6RouteInfo }) + } else { + throw 'No route found' + } + } catch (e) { + console.log('raydium route error:', e) + reject(e) + } + }, + ) } -// const fetchMangoRoutes = async ( -// inputMint = 'So11111111111111111111111111111111111111112', -// outputMint = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', -// amount = 0, -// slippage = 50, -// swapMode = 'ExactIn', -// feeBps = 0, -// wallet = PublicKey.default.toBase58(), -// ) => { -// { -// const defaultOtherAmount = -// swapMode === 'ExactIn' ? 0 : Number.MAX_SAFE_INTEGER +const fetchMangoRoute = async ( + inputMint = 'So11111111111111111111111111111111111111112', + outputMint = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + amount = 0, + slippage = 50, + swapMode = 'ExactIn', +) => { + return new Promise<{ bestRoute: JupiterV6RouteInfo }>( + // eslint-disable-next-line no-async-promise-executor + async (resolve, reject) => { + try { + const paramsString = new URLSearchParams({ + inputMint: inputMint.toString(), + outputMint: outputMint.toString(), + amount: amount.toString(), + slippageBps: Math.ceil(slippage * 100).toString(), + mode: swapMode, + }).toString() -// const paramsString = new URLSearchParams({ -// inputMint: inputMint.toString(), -// outputMint: outputMint.toString(), -// amount: amount.toString(), -// slippage: ((slippage * 1) / 100).toString(), -// feeBps: feeBps.toString(), -// mode: swapMode, -// wallet: wallet, -// otherAmountThreshold: defaultOtherAmount.toString(), -// }).toString() + const response = await fetch( + `${MANGO_ROUTER_API_URL}/quote?${paramsString}`, + ) -// const response = await fetch(`${MANGO_ROUTER_API_URL}/swap?${paramsString}`) + if (response.status === 500) { + throw 'No route found' + } + const res = await response.json() + if (res.outAmount) { + resolve({ + bestRoute: res, + }) + } else { + reject('No route found') + } + } catch (e) { + console.log('mango router error:', e) + reject(e) + } + }, + ) +} -// const res = await response.json() -// const data: RouteInfo[] = res.map((route: any) => ({ -// ...route, -// priceImpactPct: route.priceImpact, -// slippageBps: slippage, -// marketInfos: route.marketInfos.map((mInfo: any) => ({ -// ...mInfo, -// lpFee: { -// ...mInfo.fee, -// pct: mInfo.fee.rate, -// }, -// })), -// mints: route.mints.map((x: string) => new PublicKey(x)), -// instructions: route.instructions.map((ix: any) => ({ -// ...ix, -// programId: new PublicKey(ix.programId), -// data: Buffer.from(ix.data, 'base64'), -// keys: ix.keys.map((key: any) => ({ -// ...key, -// pubkey: new PublicKey(key.pubkey), -// })), -// })), -// routerName: 'Mango', -// })) -// return { -// routes: data, -// bestRoute: (data.length ? data[0] : null) as RouteInfo | null, -// } -// } -// } +export async function handleGetRoutes( + inputMint: string | undefined, + outputMint: string | undefined, + amount: number, + slippage: number, + swapMode: SwapModes, + wallet: string | undefined, + mangoAccount: MangoAccount | undefined, + routingMode: MultiRoutingMode | RaydiumRoutingMode, + connection: Connection, + inputTokenDecimals: number, +): Promise<{ bestRoute: JupiterV6RouteInfo }> -export const handleGetRoutes = async ( +export async function handleGetRoutes( + inputMint: string | undefined, + outputMint: string | undefined, + amount: number, + slippage: number, + swapMode: SwapModes, + wallet: string | undefined, + mangoAccount: MangoAccount | undefined, + routingMode: JupiterRoutingMode | MangoRoutingMode, + connection: Connection, +): Promise<{ bestRoute: JupiterV6RouteInfo }> + +export async function handleGetRoutes( + inputMint: string | undefined, + outputMint: string | undefined, + amount: number, + slippage: number, + swapMode: 'ExactIn', + wallet: string | undefined, + mangoAccount: MangoAccount | undefined, + routingMode: RaydiumRoutingMode, + connection: Connection, + inputTokenDecimals: number, +): Promise<{ bestRoute: JupiterV6RouteInfo }> + +export async function handleGetRoutes( inputMint: string | undefined, outputMint: string | undefined, amount = 0, slippage = 50, - swapMode = 'ExactIn', + swapMode: SwapModes, wallet: string | undefined, mangoAccount: MangoAccount | undefined, - mode: SwapModes = 'ALL', + routingMode: RoutingMode = 'ALL', connection: Connection, - inputTokenDecimals: number | null, - mangoAccountSwap: boolean, -) => { + inputTokenDecimals?: number, +) { try { wallet ||= PublicKey.default.toBase58() @@ -208,47 +271,74 @@ export const handleGetRoutes = async ( const routes = [] if ( - connection && - inputTokenDecimals && swapMode === 'ExactIn' && - (mode === 'ALL' || mode === 'RAYDIUM') + (isMultiRoutingMode(routingMode) || isRaydiumRoutingMode(routingMode)) ) { const raydiumRoute = fetchRaydiumRoute( inputMint, outputMint, - toUiDecimals(amount, inputTokenDecimals), + toUiDecimals(amount, inputTokenDecimals!), slippage, connection, wallet, - mangoAccountSwap, + !mangoAccount, ) - if (raydiumRoute) { - routes.push(raydiumRoute) - } + routes.push(raydiumRoute) } - if (mode === 'ALL' || mode === 'JUPITER' || mode === 'JUPITER_DIRECT') { + if ( + routingMode === 'ALL_AND_JUPITER_DIRECT' || + routingMode === 'JUPITER_DIRECT' + ) { + const jupiterDirectRoute = fetchJupiterRoute( + inputMint, + outputMint, + amount, + slippage, + swapMode, + true, + maxAccounts, + connection, + wallet, + ) + + routes.push(jupiterDirectRoute) + } + + if (isMultiRoutingMode(routingMode) || routingMode === 'JUPITER') { const jupiterRoute = fetchJupiterRoute( inputMint, outputMint, amount, slippage, swapMode, - mode === 'JUPITER_DIRECT' ? true : false, + false, maxAccounts, connection, wallet, ) - if (jupiterRoute) { - routes.push(jupiterRoute) - } + routes.push(jupiterRoute) + } + + if (isMultiRoutingMode(routingMode) || routingMode === 'MANGO') { + const mangoRoute = fetchMangoRoute( + inputMint, + outputMint, + amount, + slippage, + swapMode, + ) + routes.push(mangoRoute) } const results = await Promise.allSettled(routes) + const responses = results .filter((x) => x.status === 'fulfilled' && x.value?.bestRoute !== null) .map((x) => (x as any).value) - + if (!responses.length) { + throw 'No route found' + } const sortedByBiggestOutAmount = ( responses as { bestRoute: JupiterV6RouteInfo @@ -258,9 +348,10 @@ export const handleGetRoutes = async ( ? Number(b.bestRoute.outAmount) - Number(a.bestRoute.outAmount) : Number(a.bestRoute.inAmount) - Number(b.bestRoute.inAmount), ) - return { - bestRoute: sortedByBiggestOutAmount[0].bestRoute, + bestRoute: sortedByBiggestOutAmount.length + ? sortedByBiggestOutAmount[0]?.bestRoute + : null, } } catch (e) { return { @@ -277,8 +368,7 @@ const useQuoteRoutes = ({ swapMode, wallet, mangoAccount, - mode = 'ALL', - mangoAccountSwap, + routingMode = 'ALL', enabled, }: useQuoteRoutesPropTypes) => { const connection = mangoStore((s) => s.connection) @@ -297,29 +387,54 @@ const useQuoteRoutes = ({ const res = useQuery<{ bestRoute: JupiterV6RouteInfo | null }, Error>( [ - 'swap-routes', + [ + 'swap-routes', + nativeAmount.toString(), + inputMint, + outputMint, + swapMode, + wallet, + routingMode, + ], inputMint, outputMint, amount, slippage, swapMode, wallet, - mode, + routingMode, ], - async () => - handleGetRoutes( - inputMint, - outputMint, - nativeAmount.toNumber(), - slippage, - swapMode, - wallet, - mangoAccount, - mode, - connection, - decimals, - mangoAccountSwap, - ), + async () => { + if ( + isMultiRoutingMode(routingMode) || + isRaydiumRoutingMode(routingMode) + ) { + return handleGetRoutes( + inputMint, + outputMint, + nativeAmount.toNumber(), + slippage, + swapMode, + wallet, + mangoAccount, + routingMode, + connection, + decimals, + ) + } else { + return handleGetRoutes( + inputMint, + outputMint, + nativeAmount.toNumber(), + slippage, + swapMode, + wallet, + mangoAccount, + routingMode, + connection, + ) + } + }, { cacheTime: 1000 * 60, staleTime: 1000 * 3, diff --git a/components/trade/SpotMarketOrderSwapForm.tsx b/components/trade/SpotMarketOrderSwapForm.tsx index 7ae28a77..2ea8b8ea 100644 --- a/components/trade/SpotMarketOrderSwapForm.tsx +++ b/components/trade/SpotMarketOrderSwapForm.tsx @@ -231,8 +231,7 @@ export default function SpotMarketOrderSwapForm() { swapMode: 'ExactIn', wallet: publicKey?.toBase58(), mangoAccount, - mangoAccountSwap: true, - mode: 'JUPITER', + routingMode: 'ALL_AND_JUPITER_DIRECT', enabled: () => !!( inputBank?.mint && diff --git a/store/mangoStore.ts b/store/mangoStore.ts index c9322bd6..1c87314c 100644 --- a/store/mangoStore.ts +++ b/store/mangoStore.ts @@ -130,6 +130,10 @@ const initMangoClient = ( new PublicKey('AFrYBhb5wKQtxRS9UA9YRS4V3dwFm7SqmS6DHKq6YVgo'), //Bsol new PublicKey('7yyaeuJ1GGtVBLT2z2xub5ZWYKaNhF28mj1RdV4VDFVk'), //jitoSol new PublicKey('E4v1BBgoso9s64TQvmyownAVJbhbEPGyzA3qn4n46qj9'), //mSol + new PublicKey('8ihFLu5FimgTQ1Unh4dVyEHUGodJ5gJQCrQf4KUVB9bN'), //bonk + new PublicKey('AwpALBTXcaz2t6BayXvQQu7eZ6h7u2UNRCQNmD9ShY7Z'), //blze + new PublicKey('6ABgrEZk8urs6kJ1JNdC1sspH5zKXRqxy8sg3ZG2cQps'), //wif + new PublicKey('4dusJxxxiYrMTLGYS6cCAyu3gPn2xXLBjS7orMToZHi1'), //mnde ], multipleConnections: opts.multipleConnections, idsSource: 'api', diff --git a/utils/constants.ts b/utils/constants.ts index 270f8945..a6aca937 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -105,7 +105,7 @@ export const PROFILE_CATEGORIES = [ 'yolo', ] -export const MANGO_ROUTER_API_URL = 'https://api.mngo.cloud/router/v1' +export const MANGO_ROUTER_API_URL = 'https://router-1.fly.dev' export const MANGO_DATA_API_URL = 'https://api.mngo.cloud/data/v4' diff --git a/utils/swap/raydium.ts b/utils/swap/raydium.ts index 06f7671a..fc340be9 100644 --- a/utils/swap/raydium.ts +++ b/utils/swap/raydium.ts @@ -269,6 +269,7 @@ export const getSwapTransaction = async ( slippage, directionIn, ) + console.log(bestRoute, '@@@') const tokenInAta = getAssociatedTokenAddressSync( new PublicKey(directionIn ? bestRoute.outputMint : bestRoute.inputMint),