lev-stake-sol/hooks/useQuoteRoutes.ts

666 lines
17 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-explicit-any */
import {
AddressLookupTableAccount,
Connection,
PublicKey,
TransactionInstruction,
TransactionMessage,
VersionedTransaction,
} 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 { useMemo } from 'react'
import { JUPITER_V6_QUOTE_API_MAINNET } from 'utils/constants'
import { MangoAccount, toUiDecimals } from '@blockworks-foundation/mango-v4'
import { findRaydiumPoolInfo, getSwapTransaction } from 'utils/swap/raydium'
import mangoStore from '@store/mangoStore'
import useAnalytics from './useAnalytics'
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: SwapModes
wallet: string | undefined
mangoAccount: MangoAccount | undefined
routingMode: RoutingMode
inDecimals: number | undefined
outDecimals: number | undefined
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 deserializeJupiterIxAndAlt = async (
connection: Connection,
swapTransaction: string,
): Promise<[TransactionInstruction[], AddressLookupTableAccount[]]> => {
const parsedSwapTransaction = VersionedTransaction.deserialize(
Buffer.from(swapTransaction, 'base64'),
)
const message = parsedSwapTransaction.message
// const lookups = message.addressTableLookups
const addressLookupTablesResponses = await Promise.all(
message.addressTableLookups.map((alt) =>
connection.getAddressLookupTable(alt.accountKey),
),
)
const addressLookupTables: AddressLookupTableAccount[] =
addressLookupTablesResponses
.map((alt) => alt.value)
.filter((x): x is AddressLookupTableAccount => x !== null)
const decompiledMessage = TransactionMessage.decompile(message, {
addressLookupTableAccounts: addressLookupTables,
})
return [decompiledMessage.instructions, addressLookupTables]
}
const fetchJupiterTransaction = async (
connection: Connection,
selectedRoute: JupiterV6RouteInfo,
userPublicKey: PublicKey,
slippage: number,
inputMint: PublicKey,
outputMint: PublicKey,
origin?: 'mango' | 'jupiter' | 'raydium',
): Promise<[TransactionInstruction[], AddressLookupTableAccount[]]> => {
// docs https://station.jup.ag/api-v6/post-swap
const transactions = await (
await fetch(
`${
origin === 'mango' ? MANGO_ROUTER_API_URL : JUPITER_V6_QUOTE_API_MAINNET
}/swap`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
// response from /quote api
quoteResponse: selectedRoute,
// user public key to be used for the swap
userPublicKey,
slippageBps: Math.ceil(slippage * 100),
wrapAndUnwrapSol: false,
}),
},
)
).json()
const { swapTransaction } = transactions
const [ixs, alts] = await deserializeJupiterIxAndAlt(
connection,
swapTransaction,
)
const isSetupIx = (pk: PublicKey): boolean =>
pk.toString() === 'ComputeBudget111111111111111111111111111111' ||
pk.toString() === 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'
const isDuplicateAta = (ix: TransactionInstruction): boolean => {
return (
ix.programId.toString() ===
'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL' &&
(ix.keys[3].pubkey.toString() === inputMint.toString() ||
ix.keys[3].pubkey.toString() === outputMint.toString())
)
}
//remove ATA and compute setup from swaps in margin trades
const filtered_jup_ixs = ixs
.filter((ix) => !isSetupIx(ix.programId))
.filter((ix) => !isDuplicateAta(ix))
return [filtered_jup_ixs, alts]
}
const fetchJupiterRoute = async (
inputMint: string | undefined,
outputMint: string | undefined,
amount = 0,
slippage = 50,
swapMode: SwapModes = 'ExactIn',
onlyDirectRoutes = true,
maxAccounts = 64,
connection: Connection,
wallet: string,
sendAnalytics?: (data: object, tag: string) => Promise<void>,
) => {
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}`,
)
if (sendAnalytics) {
sendAnalytics(
{
url: `${JUPITER_V6_QUOTE_API_MAINNET}/quote?${paramsString}`,
},
'fetchJupiterRoute',
)
}
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),
'jupiter',
)
if (
maxAccounts !== 64 &&
[...ixes.flatMap((x) => x.keys.flatMap((k) => k.pubkey))].length >
maxAccounts
) {
throw 'Max accounts exceeded'
}
resolve({
bestRoute: res,
})
} catch (e) {
if (sendAnalytics) {
sendAnalytics(
{
error: `${e}`,
},
'fetchJupiterRouteError',
)
}
console.log('jupiter route error:', e)
reject(e)
}
},
)
}
const fetchRaydiumRoute = async (
inputMint: string | undefined,
outputMint: string | undefined,
amount = 0,
slippage = 50,
connection: Connection,
wallet: string,
isInWalletSwap: boolean,
sendAnalytics?: (data: object, tag: string) => Promise<void>,
) => {
return new Promise<{ bestRoute: JupiterV6RouteInfo }>(
// eslint-disable-next-line no-async-promise-executor
async (resolve, reject) => {
try {
if (sendAnalytics) {
sendAnalytics(
{
inputMint,
outputMint,
amount,
slippage,
},
'fetchRaydiumRoute',
)
}
if (!inputMint || !outputMint) return
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) {
if (sendAnalytics) {
sendAnalytics(
{
error: `${e}`,
},
'raydiumRouteError',
)
}
console.log('raydium route error:', e)
reject(e)
}
},
)
}
const fetchMangoRoute = async (
inputMint = 'So11111111111111111111111111111111111111112',
outputMint = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
amount = 0,
slippage = 50,
swapMode = 'ExactIn',
sendAnalytics?: (data: object, tag: string) => Promise<void>,
) => {
return new Promise<{ bestRoute: JupiterV6RouteInfo }>(
// eslint-disable-next-line no-async-promise-executor
async (resolve, reject) => {
const timeout = setTimeout(() => {
reject('Request timed out')
}, 5000)
try {
const paramsString = new URLSearchParams({
inputMint: inputMint.toString(),
outputMint: outputMint.toString(),
amount: amount.toString(),
slippageBps: Math.ceil(slippage * 100).toString(),
mode: swapMode,
}).toString()
const response = await fetch(
`${MANGO_ROUTER_API_URL}/quote?${paramsString}`,
)
clearTimeout(timeout)
if (sendAnalytics) {
sendAnalytics(
{
url: `${MANGO_ROUTER_API_URL}/quote?${paramsString}`,
},
'fetchMangoRoute',
)
}
if (response.status === 500) {
throw 'No route found'
}
const res = await response.json()
if (res.outAmount) {
resolve({
bestRoute: { ...res, origin: 'mango' },
})
} else {
reject('No route found')
}
} catch (e) {
clearTimeout(timeout)
if (sendAnalytics) {
sendAnalytics(
{
error: `${e}`,
},
'mangoRouteError',
)
}
console.log('mango router error:', e)
reject(e)
}
},
)
}
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,
sendAnalytics: ((data: object, tag: string) => Promise<void>) | undefined,
inputTokenDecimals: number,
): Promise<{ bestRoute: JupiterV6RouteInfo }>
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,
sendAnalytics: ((data: object, tag: string) => Promise<void>) | undefined,
): 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,
sendAnalytics: ((data: object, tag: string) => Promise<void>) | undefined,
inputTokenDecimals: number,
): Promise<{ bestRoute: JupiterV6RouteInfo }>
export async function handleGetRoutes(
inputMint: string | undefined,
outputMint: string | undefined,
amount = 0,
slippage = 50,
swapMode: SwapModes,
wallet: string | undefined,
mangoAccount: MangoAccount | undefined,
routingMode: RoutingMode = 'ALL',
connection: Connection,
sendAnalytics: ((data: object, tag: string) => Promise<void>) | undefined,
inputTokenDecimals?: number,
) {
try {
if (sendAnalytics) {
sendAnalytics(
{
inputMint,
outputMint,
amount,
slippage,
swapMode,
wallet,
routingMode,
},
'handleGetRoutes',
)
}
wallet ||= PublicKey.default.toBase58()
let maxAccounts: number
if (!mangoAccount) {
maxAccounts = 64
} else {
// TODO: replace with client method
const totalSlots =
2 * mangoAccount.tokensActive().length +
mangoAccount.serum3Active().length +
2 * mangoAccount.perpActive().length
maxAccounts = 54 - totalSlots
}
const routes = []
if (
swapMode === 'ExactIn' &&
(isMultiRoutingMode(routingMode) || isRaydiumRoutingMode(routingMode))
) {
const raydiumRoute = fetchRaydiumRoute(
inputMint,
outputMint,
toUiDecimals(amount, inputTokenDecimals!),
slippage,
connection,
wallet,
!mangoAccount,
sendAnalytics,
)
routes.push(raydiumRoute)
}
if (
routingMode === 'ALL_AND_JUPITER_DIRECT' ||
routingMode === 'JUPITER_DIRECT'
) {
const jupiterDirectRoute = fetchJupiterRoute(
inputMint,
outputMint,
amount,
slippage,
swapMode,
true,
maxAccounts,
connection,
wallet,
sendAnalytics,
)
routes.push(jupiterDirectRoute)
}
if (isMultiRoutingMode(routingMode) || routingMode === 'JUPITER') {
const jupiterRoute = fetchJupiterRoute(
inputMint,
outputMint,
amount,
slippage,
swapMode,
false,
maxAccounts,
connection,
wallet,
sendAnalytics,
)
routes.push(jupiterRoute)
}
if (isMultiRoutingMode(routingMode) || routingMode === 'MANGO') {
const mangoRoute = fetchMangoRoute(
inputMint,
outputMint,
amount,
slippage,
swapMode,
sendAnalytics,
)
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
}[]
).sort((a, b) =>
swapMode === 'ExactIn'
? Number(b.bestRoute.outAmount) - Number(a.bestRoute.outAmount)
: Number(a.bestRoute.inAmount) - Number(b.bestRoute.inAmount),
)
return {
bestRoute: sortedByBiggestOutAmount.length
? sortedByBiggestOutAmount[0]?.bestRoute
: null,
}
} catch (e) {
if (sendAnalytics) {
sendAnalytics(
{
error: `${e}`,
},
'noRouteFoundError',
)
}
return {
bestRoute: null,
}
}
}
const useQuoteRoutes = ({
inputMint,
outputMint,
amount,
slippage,
swapMode,
wallet,
mangoAccount,
routingMode = 'ALL',
inDecimals,
outDecimals,
enabled,
}: useQuoteRoutesPropTypes) => {
const connection = mangoStore((s) => s.connection)
const { sendAnalytics } = useAnalytics()
const decimals = useMemo(() => {
return swapMode === 'ExactIn' ? inDecimals || 6 : outDecimals || 6
}, [swapMode, inDecimals, outDecimals])
const nativeAmount = useMemo(() => {
return amount && !Number.isNaN(+amount)
? new Decimal(amount).mul(10 ** decimals)
: new Decimal(0)
}, [amount, decimals])
const res = useQuery<{ bestRoute: JupiterV6RouteInfo | null }, Error>(
[
[
'swap-routes',
nativeAmount.toString(),
inputMint,
outputMint,
swapMode,
wallet,
routingMode,
],
inputMint,
outputMint,
amount,
slippage,
swapMode,
wallet,
routingMode,
],
async () => {
if (
isMultiRoutingMode(routingMode) ||
isRaydiumRoutingMode(routingMode)
) {
return handleGetRoutes(
inputMint,
outputMint,
nativeAmount.toNumber(),
slippage,
swapMode,
wallet,
mangoAccount,
routingMode,
connection,
sendAnalytics,
decimals,
)
} else {
return handleGetRoutes(
inputMint,
outputMint,
nativeAmount.toNumber(),
slippage,
swapMode,
wallet,
mangoAccount,
routingMode,
connection,
sendAnalytics,
)
}
},
{
cacheTime: 1000 * 60,
staleTime: 1000 * 3,
enabled: enabled
? enabled()
: nativeAmount.toNumber() && inputMint && outputMint
? true
: false,
refetchInterval: 20000,
retry: 3,
},
)
return amount
? {
...(res.data ?? {
routes: [],
bestRoute: undefined,
}),
isFetching: res.isFetching,
isLoading: res.isLoading,
isInitialLoading: res.isInitialLoading,
refetch: res.refetch,
}
: {
routes: [],
bestRoute: undefined,
isFetching: false,
isLoading: false,
isInitialLoading: false,
refetch: undefined,
}
}
export default useQuoteRoutes