max accounts fix (#424)

* raydium instruction amm

* adjustments

* fix

* fix

* fix
This commit is contained in:
Adrian Brzeziński 2024-05-18 15:16:08 +02:00 committed by GitHub
parent 0b61387829
commit 2462f5ee14
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 468 additions and 54 deletions

View File

@ -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(

View File

@ -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[])

View File

@ -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<boolean>(false)
const [swapRate, setSwapRate] = useState<boolean>(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({

View File

@ -69,6 +69,7 @@ const WalletSwapForm = ({ setShowTokenSelect }: WalletSwapFormProps) => {
swapMode,
wallet: publicKey?.toBase58(),
mangoAccount: undefined,
mangoAccountSwap: false,
mode: 'JUPITER',
enabled: () =>
!!(

View File

@ -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,

View File

@ -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({

View File

@ -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 {

324
utils/swap/raydium.ts Normal file
View File

@ -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<GetProgramAccountsResponse> => {
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<LiquidityPoolKeys | undefined> => {
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())
)
}