diff --git a/components/governance/ListToken/ListToken.tsx b/components/governance/ListToken/ListToken.tsx index e2dffc49..cc83f6ae 100644 --- a/components/governance/ListToken/ListToken.tsx +++ b/components/governance/ListToken/ListToken.tsx @@ -342,11 +342,13 @@ const ListToken = ({ goBack }: { goBack: () => void }) => { mode, walletForCheck, undefined, // mangoAccount - 'JUPITER', - onlyDirect, + onlyDirect ? 'JUPITER_DIRECT' : 'JUPITER', + connection, + null, + false, ) }, - [wallet.publicKey], + [wallet.publicKey, connection], ) const handleLiquidityCheck = useCallback( diff --git a/components/swap/MarketSwapForm.tsx b/components/swap/MarketSwapForm.tsx index ad689b82..f16bfd64 100644 --- a/components/swap/MarketSwapForm.tsx +++ b/components/swap/MarketSwapForm.tsx @@ -83,6 +83,7 @@ const MarketSwapForm = ({ swapMode, wallet: publicKey?.toBase58(), mangoAccount, + mangoAccountSwap: true, enabled: () => !!( inputBank?.mint && @@ -100,6 +101,7 @@ const MarketSwapForm = ({ swapMode, wallet: publicKey?.toBase58(), mangoAccount, + mangoAccountSwap: true, mode: 'JUPITER_DIRECT', enabled: () => !!( @@ -231,22 +233,26 @@ const MarketSwapForm = ({ depending on the swapMode and set those values in state */ useEffect(() => { - if (typeof bestRoute !== 'undefined') { - setSelectedRoute(bestRoute) + if ( + typeof bestRoute !== 'undefined' || + typeof bestDirectRoute !== 'undefined' + ) { + const newRoute = bestRoute || bestDirectRoute + setSelectedRoute(newRoute) - if (inputBank && swapMode === 'ExactOut' && bestRoute?.inAmount) { - const inAmount = new Decimal(bestRoute.inAmount) + if (inputBank && swapMode === 'ExactOut' && newRoute?.inAmount) { + const inAmount = new Decimal(newRoute.inAmount) .div(10 ** inputBank.mintDecimals) .toString() setAmountInFormValue(inAmount) - } else if (outputBank && swapMode === 'ExactIn' && bestRoute?.outAmount) { - const outAmount = new Decimal(bestRoute.outAmount) + } else if (outputBank && swapMode === 'ExactIn' && newRoute?.outAmount) { + const outAmount = new Decimal(newRoute.outAmount) .div(10 ** outputBank.mintDecimals) .toString() setAmountOutFormValue(outAmount) } } - }, [bestRoute, swapMode, inputBank, outputBank]) + }, [bestRoute, bestDirectRoute, swapMode, inputBank, outputBank]) const handleSwitchTokens = useCallback(() => { if (amountInAsDecimal?.gt(0) && amountOutAsDecimal.gte(0)) { @@ -312,7 +318,7 @@ const MarketSwapForm = ({ onSuccess={onSuccess} refetchRoute={refetchRoute} routes={ - bestRoute + bestRoute || bestDirectRoute ? ([bestRoute, bestDirectRoute].filter( (x) => x && !x.error, ) as JupiterV6RouteInfo[]) diff --git a/components/swap/SwapReviewRouteInfo.tsx b/components/swap/SwapReviewRouteInfo.tsx index ccec4a9f..54ae3b92 100644 --- a/components/swap/SwapReviewRouteInfo.tsx +++ b/components/swap/SwapReviewRouteInfo.tsx @@ -286,6 +286,7 @@ const SwapReviewRouteInfo = ({ }: JupiterRouteInfoProps) => { const { t } = useTranslation(['common', 'account', 'swap', 'trade']) const slippage = mangoStore((s) => s.swap.slippage) + const wallet = useWallet() const [showRoutesModal, setShowRoutesModal] = useState(false) const [swapRate, setSwapRate] = useState(false) @@ -429,6 +430,7 @@ const SwapReviewRouteInfo = ({ ) return setSubmitting(true) + const [ixs, alts] = // selectedRoute.routerName === 'Mango' // ? await prepareMangoRouterInstructions( @@ -438,14 +440,16 @@ const SwapReviewRouteInfo = ({ // mangoAccount.owner, // ) // : - await fetchJupiterTransaction( - connection, - selectedRoute, - wallet.publicKey, - slippage, - inputBank.mint, - outputBank.mint, - ) + selectedRoute.instructions + ? [selectedRoute.instructions, []] + : await fetchJupiterTransaction( + connection, + selectedRoute, + wallet.publicKey, + slippage, + inputBank.mint, + outputBank.mint, + ) try { const { signature: tx, slot } = await client.marginTrade({ diff --git a/components/swap/WalletSwapForm.tsx b/components/swap/WalletSwapForm.tsx index fafe1bab..3c5f6b6f 100644 --- a/components/swap/WalletSwapForm.tsx +++ b/components/swap/WalletSwapForm.tsx @@ -69,6 +69,7 @@ const WalletSwapForm = ({ setShowTokenSelect }: WalletSwapFormProps) => { swapMode, wallet: publicKey?.toBase58(), mangoAccount: undefined, + mangoAccountSwap: false, mode: 'JUPITER', enabled: () => !!( diff --git a/components/swap/useQuoteRoutes.ts b/components/swap/useQuoteRoutes.ts index 14e3333e..839ace76 100644 --- a/components/swap/useQuoteRoutes.ts +++ b/components/swap/useQuoteRoutes.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { PublicKey } from '@solana/web3.js' +import { Connection, PublicKey } from '@solana/web3.js' import { useQuery } from '@tanstack/react-query' import Decimal from 'decimal.js' import { JupiterV6RouteInfo } from 'types/jupiter' @@ -7,9 +7,12 @@ import { JupiterV6RouteInfo } from 'types/jupiter' import useJupiterSwapData from './useJupiterSwapData' import { useMemo } from 'react' import { JUPITER_V6_QUOTE_API_MAINNET } from 'utils/constants' -import { MangoAccount } from '@blockworks-foundation/mango-v4' +import { MangoAccount, toUiDecimals } from '@blockworks-foundation/mango-v4' +import { findRaydiumPoolInfo, getSwapTransaction } from 'utils/swap/raydium' +import mangoStore from '@store/mangoStore' +import { fetchJupiterTransaction } from './SwapReviewRouteInfo' -type SwapModes = 'ALL' | 'JUPITER' | 'MANGO' | 'JUPITER_DIRECT' +type SwapModes = 'ALL' | 'JUPITER' | 'MANGO' | 'JUPITER_DIRECT' | 'RAYDIUM' type useQuoteRoutesPropTypes = { inputMint: string | undefined @@ -20,6 +23,7 @@ type useQuoteRoutesPropTypes = { wallet: string | undefined mangoAccount: MangoAccount | undefined mode?: SwapModes + mangoAccountSwap: boolean enabled?: () => boolean } @@ -31,6 +35,8 @@ const fetchJupiterRoute = async ( swapMode = 'ExactIn', onlyDirectRoutes = true, maxAccounts = 64, + connection: Connection, + wallet: string, ) => { if (!inputMint || !outputMint) return try { @@ -60,8 +66,20 @@ const fetchJupiterRoute = async ( `${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: res, + bestRoute: + [...ixes.flatMap((x) => x.keys.flatMap((k) => k.pubkey))].length <= + maxAccounts + ? res + : undefined, } } } catch (e) { @@ -69,6 +87,39 @@ const fetchJupiterRoute = async ( } } +const fetchRaydiumRoute = async ( + inputMint: string | undefined, + outputMint: string | undefined, + amount = 0, + slippage = 50, + connection: Connection, + wallet: string, + mangoAccountSwap: boolean, +) => { + if (!inputMint || !outputMint) return + try { + const poolKeys = await findRaydiumPoolInfo( + connection, + outputMint, + inputMint, + ) + + if (poolKeys) { + return await getSwapTransaction( + connection, + outputMint, + amount, + poolKeys!, + slippage, + new PublicKey(wallet), + mangoAccountSwap, + ) + } + } catch (e) { + console.log('error fetching raydium route', e) + } +} + // const fetchMangoRoutes = async ( // inputMint = 'So11111111111111111111111111111111111111112', // outputMint = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', @@ -135,7 +186,9 @@ export const handleGetRoutes = async ( wallet: string | undefined, mangoAccount: MangoAccount | undefined, mode: SwapModes = 'ALL', - jupiterOnlyDirectRoutes = false, + connection: Connection, + inputTokenDecimals: number | null, + mangoAccountSwap: boolean, ) => { try { wallet ||= PublicKey.default.toBase58() @@ -154,35 +207,41 @@ export const handleGetRoutes = async ( const routes = [] - // FIXME: Disable for now, mango router needs to use ALTs - // if (mode === 'ALL' || mode === 'MANGO') { - // const mangoRoute = fetchMangoRoutes( - // inputMint, - // outputMint, - // amount, - // slippage, - // swapMode, - // feeBps, - // wallet, - // ) - // routes.push(mangoRoute) - // } + if ( + connection && + inputTokenDecimals && + swapMode === 'ExactIn' && + (mode === 'ALL' || mode === 'RAYDIUM') + ) { + const raydiumRoute = fetchRaydiumRoute( + inputMint, + outputMint, + toUiDecimals(amount, inputTokenDecimals), + slippage, + connection, + wallet, + mangoAccountSwap, + ) + if (raydiumRoute) { + routes.push(raydiumRoute) + } + } if (mode === 'ALL' || mode === 'JUPITER' || mode === 'JUPITER_DIRECT') { - const jupiterRoute = await fetchJupiterRoute( + const jupiterRoute = fetchJupiterRoute( inputMint, outputMint, amount, slippage, swapMode, - jupiterOnlyDirectRoutes - ? jupiterOnlyDirectRoutes - : mode === 'JUPITER_DIRECT' - ? true - : false, + mode === 'JUPITER_DIRECT' ? true : false, maxAccounts, + connection, + wallet, ) - routes.push(jupiterRoute) + if (jupiterRoute) { + routes.push(jupiterRoute) + } } const results = await Promise.allSettled(routes) @@ -199,6 +258,7 @@ 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, } @@ -218,8 +278,10 @@ const useQuoteRoutes = ({ wallet, mangoAccount, mode = 'ALL', + mangoAccountSwap, enabled, }: useQuoteRoutesPropTypes) => { + const connection = mangoStore((s) => s.connection) const { inputTokenInfo, outputTokenInfo } = useJupiterSwapData() const decimals = useMemo(() => { return swapMode === 'ExactIn' @@ -254,6 +316,9 @@ const useQuoteRoutes = ({ wallet, mangoAccount, mode, + connection, + decimals, + mangoAccountSwap, ), { cacheTime: 1000 * 60, diff --git a/components/trade/SpotMarketOrderSwapForm.tsx b/components/trade/SpotMarketOrderSwapForm.tsx index 03b75194..7ae28a77 100644 --- a/components/trade/SpotMarketOrderSwapForm.tsx +++ b/components/trade/SpotMarketOrderSwapForm.tsx @@ -231,6 +231,7 @@ export default function SpotMarketOrderSwapForm() { swapMode: 'ExactIn', wallet: publicKey?.toBase58(), mangoAccount, + mangoAccountSwap: true, mode: 'JUPITER', enabled: () => !!( @@ -260,15 +261,25 @@ export default function SpotMarketOrderSwapForm() { return setPlacingOrder(true) - - const [ixs, alts] = await fetchJupiterTransaction( - connection, - selectedRoute, - publicKey, - slippage, - inputBank.mint, - outputBank.mint, - ) + const [ixs, alts] = + // selectedRoute.routerName === 'Mango' + // ? await prepareMangoRouterInstructions( + // selectedRoute, + // inputBank.mint, + // outputBank.mint, + // mangoAccount.owner, + // ) + // : + selectedRoute.instructions + ? [selectedRoute.instructions, []] + : await fetchJupiterTransaction( + connection, + selectedRoute, + publicKey, + slippage, + inputBank.mint, + outputBank.mint, + ) try { const { signature: tx, slot } = await client.marginTrade({ diff --git a/types/jupiter.ts b/types/jupiter.ts index 0864660b..78952ac8 100644 --- a/types/jupiter.ts +++ b/types/jupiter.ts @@ -1,4 +1,4 @@ -import { AccountInfo } from '@solana/web3.js' +import { AccountInfo, TransactionInstruction } from '@solana/web3.js' export declare type SideType = typeof Side.Ask | typeof Side.Bid export declare const Side: { @@ -123,6 +123,7 @@ export interface JupiterV6RouteInfo { contextSlot?: number timeTaken?: number error?: string + instructions?: TransactionInstruction[] } export interface JupiterV6RoutePlan { diff --git a/utils/swap/raydium.ts b/utils/swap/raydium.ts new file mode 100644 index 00000000..06f7671a --- /dev/null +++ b/utils/swap/raydium.ts @@ -0,0 +1,324 @@ +import { + LIQUIDITY_STATE_LAYOUT_V4, + LiquidityPoolKeys, + Liquidity, + Token, + TokenAmount, + Percent, + MARKET_STATE_LAYOUT_V3, + Market, + CurrencyAmount, + Price, +} from '@raydium-io/raydium-sdk' +import { TOKEN_PROGRAM_ID } from '@solana/spl-governance' +import { getAssociatedTokenAddressSync } from '@solana/spl-token' +import { + Connection, + GetProgramAccountsResponse, + PublicKey, + TransactionInstruction, +} from '@solana/web3.js' +import BN from 'bn.js' + +const RAYDIUM_V4_PROGRAM_ID = '675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8' + +const _getProgramAccounts = ( + connection: Connection, + baseMint: string, + quoteMint: string, +): Promise => { + const layout = LIQUIDITY_STATE_LAYOUT_V4 + + return connection.getProgramAccounts(new PublicKey(RAYDIUM_V4_PROGRAM_ID), { + filters: [ + { dataSize: layout.span }, + { + memcmp: { + offset: layout.offsetOf('baseMint'), + bytes: new PublicKey(baseMint).toBase58(), + }, + }, + { + memcmp: { + offset: layout.offsetOf('quoteMint'), + bytes: new PublicKey(quoteMint).toBase58(), + }, + }, + ], + }) +} + +const getProgramAccounts = async ( + connection: Connection, + baseMint: string, + quoteMint: string, +) => { + const response = await Promise.all([ + _getProgramAccounts(connection, baseMint, quoteMint), + _getProgramAccounts(connection, quoteMint, baseMint), + ]) + + return response.filter((r) => r.length > 0).flatMap((x) => x) +} + +export const findRaydiumPoolInfo = async ( + connection: Connection, + baseMint: string, + quoteMint: string, +): Promise => { + const layout = LIQUIDITY_STATE_LAYOUT_V4 + + const programData = await getProgramAccounts(connection, baseMint, quoteMint) + + const collectedPoolResults = programData + .map((info) => ({ + id: new PublicKey(info.pubkey), + version: 4, + programId: new PublicKey(RAYDIUM_V4_PROGRAM_ID), + ...layout.decode(info.account.data), + })) + .flat() + + const pools = await Promise.all([ + fetch(`https://api.dexscreener.com/latest/dex/search?q=${baseMint}`), + fetch(`https://api.dexscreener.com/latest/dex/search?q=${quoteMint}`), + ]) + const resp = await Promise.all([...pools.map((x) => x.json())]) + + const bestDexScannerPoolId = resp + .flatMap((x) => x.pairs) + .find( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (x: any) => + x.dexId === 'raydium' && + ((x.baseToken.address === baseMint && + x.quoteToken.address === quoteMint) || + (x.baseToken.address === quoteMint && + x.quoteToken.address === baseMint)), + )?.pairAddress + + const pool = collectedPoolResults.find( + (x) => x.id.toBase58() === bestDexScannerPoolId, + ) + + if (!pool) return undefined + + const market = await connection + .getAccountInfo(pool.marketId) + .then((item) => ({ + programId: item!.owner, + ...MARKET_STATE_LAYOUT_V3.decode(item!.data), + })) + + const authority = Liquidity.getAssociatedAuthority({ + programId: new PublicKey(RAYDIUM_V4_PROGRAM_ID), + }).publicKey + + const marketProgramId = market.programId + + const poolKeys = { + id: pool.id, + baseMint: pool.baseMint, + quoteMint: pool.quoteMint, + lpMint: pool.lpMint, + baseDecimals: Number.parseInt(pool.baseDecimal.toString()), + quoteDecimals: Number.parseInt(pool.quoteDecimal.toString()), + lpDecimals: Number.parseInt(pool.baseDecimal.toString()), + version: pool.version, + programId: pool.programId, + openOrders: pool.openOrders, + targetOrders: pool.targetOrders, + baseVault: pool.baseVault, + quoteVault: pool.quoteVault, + marketVersion: 3, + authority: authority, + marketProgramId, + marketId: market.ownAddress, + marketAuthority: Market.getAssociatedAuthority({ + programId: marketProgramId, + marketId: market.ownAddress, + }).publicKey, + marketBaseVault: market.baseVault, + marketQuoteVault: market.quoteVault, + marketBids: market.bids, + marketAsks: market.asks, + marketEventQueue: market.eventQueue, + withdrawQueue: pool.withdrawQueue, + lpVault: pool.lpVault, + lookupTableAccount: PublicKey.default, + } as LiquidityPoolKeys + + return poolKeys +} + +const calcAmountOut = async ( + connection: Connection, + poolKeys: LiquidityPoolKeys, + rawAmountIn: number, + slippage = 5, + swapInDirection: boolean, +) => { + const poolInfo = await Liquidity.fetchInfo({ + connection: connection, + poolKeys, + }) + + let currencyInMint = poolKeys.baseMint + let currencyInDecimals = poolInfo.baseDecimals + let currencyOutMint = poolKeys.quoteMint + let currencyOutDecimals = poolInfo.quoteDecimals + + if (!swapInDirection) { + currencyInMint = poolKeys.quoteMint + currencyInDecimals = poolInfo.quoteDecimals + currencyOutMint = poolKeys.baseMint + currencyOutDecimals = poolInfo.baseDecimals + } + + const currencyIn = new Token( + TOKEN_PROGRAM_ID, + currencyInMint, + currencyInDecimals, + ) + const amountIn = new TokenAmount( + currencyIn, + rawAmountIn.toFixed(currencyInDecimals), + false, + ) + const currencyOut = new Token( + TOKEN_PROGRAM_ID, + currencyOutMint, + currencyOutDecimals, + ) + const slippageX = new Percent(Math.ceil(slippage * 10), 1000) + + const { + amountOut, + minAmountOut, + currentPrice, + executionPrice, + priceImpact, + fee, + } = Liquidity.computeAmountOut({ + poolKeys, + poolInfo, + amountIn, + currencyOut, + slippage: slippageX, + }) + + return { + amountIn: amountIn, + amountOut: amountOut, + inAmount: amountIn.raw.toNumber(), + outAmount: amountOut.raw.toNumber(), + otherAmountThreshold: minAmountOut.raw.toNumber(), + minAmountOut: minAmountOut, + currentPrice, + executionPrice, + priceImpactPct: Number(priceImpact.toSignificant()) / 100, + fee, + inputMint: poolKeys.quoteMint.toBase58(), + outputMint: poolKeys.baseMint.toBase58(), + routePlan: [ + { + swapInfo: { + ammKey: poolKeys.id.toBase58(), + label: 'Raydium', + inputMint: poolKeys.quoteMint.toBase58(), + outputMint: poolKeys.baseMint.toBase58(), + inAmount: amountIn.raw.toNumber(), + outAmount: amountOut.raw.toNumber(), + feeAmount: 0, + feeMint: poolKeys.lpMint.toBase58(), + }, + percent: 100, + }, + ], + } +} + +export const getSwapTransaction = async ( + connection: Connection, + toToken: string, + amount: number, + poolKeys: LiquidityPoolKeys, + slippage = 5, + wallet: PublicKey, + mangoAccountSwap: boolean, +): Promise<{ + bestRoute: { + amountIn: TokenAmount + amountOut: TokenAmount | CurrencyAmount + inAmount: number + outAmount: number + otherAmountThreshold: number + currentPrice: Price + executionPrice: Price | null + priceImpactPct: number + fee: CurrencyAmount + instructions: TransactionInstruction[] + } +}> => { + const directionIn = poolKeys.quoteMint.toString() == toToken + + const bestRoute = await calcAmountOut( + connection, + poolKeys, + amount, + slippage, + directionIn, + ) + + const tokenInAta = getAssociatedTokenAddressSync( + new PublicKey(directionIn ? bestRoute.outputMint : bestRoute.inputMint), + wallet, + ) + const tokenOutAta = getAssociatedTokenAddressSync( + new PublicKey(directionIn ? bestRoute.inputMint : bestRoute.outputMint), + wallet, + ) + const swapTransaction = Liquidity.makeSwapInstruction({ + poolKeys: { + ...poolKeys, + }, + userKeys: { + tokenAccountIn: tokenInAta, + tokenAccountOut: tokenOutAta, + owner: wallet, + }, + amountIn: bestRoute.amountIn.raw, + amountOut: bestRoute.minAmountOut.raw.sub(bestRoute.fee?.raw ?? new BN(0)), + fixedSide: !directionIn ? 'in' : 'out', + }) + + const instructions = + swapTransaction.innerTransaction.instructions.filter(Boolean) + + const filtered_instructions = mangoAccountSwap + ? instructions + .filter((ix) => !isSetupIx(ix.programId)) + .filter( + (ix) => !isDuplicateAta(ix, poolKeys.baseMint, poolKeys.quoteMint), + ) + : instructions + + return { bestRoute: { ...bestRoute, instructions: filtered_instructions } } +} + +const isSetupIx = (pk: PublicKey): boolean => + pk.toString() === 'ComputeBudget111111111111111111111111111111' || + pk.toString() === 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA' + +const isDuplicateAta = ( + ix: TransactionInstruction, + inputMint: PublicKey, + outputMint: PublicKey, +): boolean => { + return ( + ix.programId.toString() === + 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL' && + (ix.keys[3].pubkey.toString() === inputMint.toString() || + ix.keys[3].pubkey.toString() === outputMint.toString()) + ) +}