mango-v4-ui/utils/swap/raydium.ts

281 lines
7.5 KiB
TypeScript

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, PublicKey, TransactionInstruction } from '@solana/web3.js'
import BN from 'bn.js'
const RAYDIUM_V4_PROGRAM_ID = '675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8'
export const findRaydiumPoolInfo = async (
connection: Connection,
baseMint: string,
quoteMint: string,
): Promise<LiquidityPoolKeys | undefined> => {
const layout = LIQUIDITY_STATE_LAYOUT_V4
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
if (!bestDexScannerPoolId) return undefined
const pool = await connection
.getAccountInfo(new PublicKey(bestDexScannerPoolId))
.then((item) =>
item
? {
id: new PublicKey(bestDexScannerPoolId),
version: 4,
programId: new PublicKey(RAYDIUM_V4_PROGRAM_ID),
...layout.decode(item.data),
}
: null,
)
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())
)
}