Merge pull request #61 from blockworks-foundation/feature/mang-router-v2
mango router
This commit is contained in:
commit
5e17c81df7
|
@ -23,7 +23,7 @@ import { Transition } from '@headlessui/react'
|
|||
import Button, { IconButton } from '../shared/Button'
|
||||
import Loading from '../shared/Loading'
|
||||
import { EnterBottomExitBottom } from '../shared/Transitions'
|
||||
import useJupiterRoutes from './useJupiterRoutes'
|
||||
import useQuoteRoutes from './useQuoteRoutes'
|
||||
import SheenLoader from '../shared/SheenLoader'
|
||||
import { HealthType } from '@blockworks-foundation/mango-v4'
|
||||
import {
|
||||
|
@ -80,7 +80,7 @@ const SwapForm = () => {
|
|||
const [debouncedAmountIn] = useDebounce(amountInFormValue, 300)
|
||||
const [debouncedAmountOut] = useDebounce(amountOutFormValue, 300)
|
||||
const { mangoAccount } = useMangoAccount()
|
||||
const { connected } = useWallet()
|
||||
const { connected, publicKey } = useWallet()
|
||||
|
||||
const amountInAsDecimal: Decimal | null = useMemo(() => {
|
||||
return Number(debouncedAmountIn)
|
||||
|
@ -94,12 +94,13 @@ const SwapForm = () => {
|
|||
: new Decimal(0)
|
||||
}, [debouncedAmountOut])
|
||||
|
||||
const { bestRoute, routes } = useJupiterRoutes({
|
||||
const { bestRoute, routes } = useQuoteRoutes({
|
||||
inputMint: inputBank?.mint.toString() || USDC_MINT,
|
||||
outputMint: outputBank?.mint.toString() || MANGO_MINT,
|
||||
amount: swapMode === 'ExactIn' ? debouncedAmountIn : debouncedAmountOut,
|
||||
slippage,
|
||||
swapMode,
|
||||
wallet: publicKey?.toBase58(),
|
||||
})
|
||||
|
||||
const setAmountInFormValue = useCallback(
|
||||
|
|
|
@ -42,6 +42,7 @@ import Tooltip from '@components/shared/Tooltip'
|
|||
import { Disclosure } from '@headlessui/react'
|
||||
import RoutesModal from './RoutesModal'
|
||||
import useMangoAccount from 'hooks/useMangoAccount'
|
||||
import { createAssociatedTokenAccountIdempotentInstruction } from '@blockworks-foundation/mango-v4'
|
||||
|
||||
type JupiterRouteInfoProps = {
|
||||
amountIn: Decimal
|
||||
|
@ -78,6 +79,40 @@ const deserializeJupiterIxAndAlt = async (
|
|||
return [decompiledMessage.instructions, addressLookupTables]
|
||||
}
|
||||
|
||||
const prepareMangoRouterInstructions = async (
|
||||
selectedRoute: RouteInfo,
|
||||
inputMint: PublicKey,
|
||||
outputMint: PublicKey,
|
||||
userPublicKey: PublicKey
|
||||
): Promise<[TransactionInstruction[], AddressLookupTableAccount[]]> => {
|
||||
if (!selectedRoute || !selectedRoute.mints || !selectedRoute.instructions) {
|
||||
return [[], []]
|
||||
}
|
||||
const mintsToFilterOut = [inputMint, outputMint]
|
||||
const filteredOutMints = [
|
||||
...selectedRoute.mints.filter(
|
||||
(routeMint) =>
|
||||
!mintsToFilterOut.find((filterOutMint) =>
|
||||
filterOutMint.equals(routeMint)
|
||||
)
|
||||
),
|
||||
]
|
||||
const additionalInstructions = []
|
||||
for (const mint of filteredOutMints) {
|
||||
const ix = await createAssociatedTokenAccountIdempotentInstruction(
|
||||
userPublicKey,
|
||||
userPublicKey,
|
||||
mint
|
||||
)
|
||||
additionalInstructions.push(ix)
|
||||
}
|
||||
const instructions = [
|
||||
...additionalInstructions,
|
||||
...selectedRoute.instructions,
|
||||
]
|
||||
return [instructions, []]
|
||||
}
|
||||
|
||||
const fetchJupiterTransaction = async (
|
||||
connection: Connection,
|
||||
selectedRoute: RouteInfo,
|
||||
|
@ -217,14 +252,22 @@ const SwapReviewRouteInfo = ({
|
|||
|
||||
if (!mangoAccount || !group || !inputBank || !outputBank) return
|
||||
setSubmitting(true)
|
||||
const [ixs, alts] = await fetchJupiterTransaction(
|
||||
connection,
|
||||
selectedRoute,
|
||||
mangoAccount.owner,
|
||||
slippage,
|
||||
inputBank.mint,
|
||||
outputBank.mint
|
||||
)
|
||||
const [ixs, alts] =
|
||||
selectedRoute.routerName === 'Mango'
|
||||
? await prepareMangoRouterInstructions(
|
||||
selectedRoute,
|
||||
inputBank.mint,
|
||||
outputBank.mint,
|
||||
mangoAccount.owner
|
||||
)
|
||||
: await fetchJupiterTransaction(
|
||||
connection,
|
||||
selectedRoute,
|
||||
mangoAccount.owner,
|
||||
slippage,
|
||||
inputBank.mint,
|
||||
outputBank.mint
|
||||
)
|
||||
|
||||
try {
|
||||
const tx = await client.marginTrade({
|
||||
|
|
|
@ -1,95 +0,0 @@
|
|||
import { useQuery } from '@tanstack/react-query'
|
||||
import Decimal from 'decimal.js'
|
||||
import { RouteInfo } from 'types/jupiter'
|
||||
import useJupiterSwapData from './useJupiterSwapData'
|
||||
|
||||
type useJupiterPropTypes = {
|
||||
inputMint: string
|
||||
outputMint: string
|
||||
amount: string
|
||||
slippage: number
|
||||
swapMode: string
|
||||
}
|
||||
|
||||
const fetchJupiterRoutes = async (
|
||||
inputMint = 'So11111111111111111111111111111111111111112',
|
||||
outputMint = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
|
||||
amount = 0,
|
||||
slippage = 50,
|
||||
swapMode = 'ExactIn',
|
||||
feeBps = 0
|
||||
) => {
|
||||
{
|
||||
const paramsString = new URLSearchParams({
|
||||
inputMint: inputMint.toString(),
|
||||
outputMint: outputMint.toString(),
|
||||
amount: amount.toString(),
|
||||
slippageBps: Math.ceil(slippage * 100).toString(),
|
||||
feeBps: feeBps.toString(),
|
||||
swapMode,
|
||||
}).toString()
|
||||
|
||||
const response = await fetch(
|
||||
`https://quote-api.jup.ag/v4/quote?${paramsString}`
|
||||
)
|
||||
|
||||
const res = await response.json()
|
||||
const data = res.data
|
||||
|
||||
return {
|
||||
routes: res.data,
|
||||
bestRoute: data[0],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const useJupiterRoutes = ({
|
||||
inputMint,
|
||||
outputMint,
|
||||
amount,
|
||||
slippage,
|
||||
swapMode,
|
||||
}: useJupiterPropTypes) => {
|
||||
const { inputTokenInfo, outputTokenInfo } = useJupiterSwapData()
|
||||
|
||||
const decimals =
|
||||
swapMode === 'ExactIn'
|
||||
? inputTokenInfo?.decimals || 6
|
||||
: outputTokenInfo?.decimals || 6
|
||||
|
||||
const nativeAmount =
|
||||
amount && !Number.isNaN(+amount)
|
||||
? new Decimal(amount).mul(10 ** decimals)
|
||||
: new Decimal(0)
|
||||
|
||||
const res = useQuery<{ routes: RouteInfo[]; bestRoute: RouteInfo }, Error>(
|
||||
['swap-routes', inputMint, outputMint, amount, slippage, swapMode],
|
||||
async () =>
|
||||
fetchJupiterRoutes(
|
||||
inputMint,
|
||||
outputMint,
|
||||
nativeAmount.toNumber(),
|
||||
slippage,
|
||||
swapMode
|
||||
),
|
||||
{
|
||||
enabled: amount ? true : false,
|
||||
}
|
||||
)
|
||||
|
||||
return amount
|
||||
? {
|
||||
...(res.data ?? {
|
||||
routes: [],
|
||||
bestRoute: undefined,
|
||||
}),
|
||||
isLoading: res.isLoading,
|
||||
}
|
||||
: {
|
||||
routes: [],
|
||||
bestRoute: undefined,
|
||||
isLoading: false,
|
||||
}
|
||||
}
|
||||
|
||||
export default useJupiterRoutes
|
|
@ -0,0 +1,207 @@
|
|||
import { PublicKey } from '@solana/web3.js'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import Decimal from 'decimal.js'
|
||||
import { RouteInfo } from 'types/jupiter'
|
||||
import { MANGO_ROUTER_API_URL } from 'utils/constants'
|
||||
import useJupiterSwapData from './useJupiterSwapData'
|
||||
|
||||
type useQuoteRoutesPropTypes = {
|
||||
inputMint: string
|
||||
outputMint: string
|
||||
amount: string
|
||||
slippage: number
|
||||
swapMode: string
|
||||
wallet: string | undefined | null
|
||||
}
|
||||
|
||||
const fetchJupiterRoutes = async (
|
||||
inputMint = 'So11111111111111111111111111111111111111112',
|
||||
outputMint = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
|
||||
amount = 0,
|
||||
slippage = 50,
|
||||
swapMode = 'ExactIn',
|
||||
feeBps = 0
|
||||
) => {
|
||||
{
|
||||
const paramsString = new URLSearchParams({
|
||||
inputMint: inputMint.toString(),
|
||||
outputMint: outputMint.toString(),
|
||||
amount: amount.toString(),
|
||||
slippageBps: Math.ceil(slippage * 100).toString(),
|
||||
feeBps: feeBps.toString(),
|
||||
swapMode,
|
||||
}).toString()
|
||||
|
||||
const response = await fetch(
|
||||
`https://quote-api.jup.ag/v4/quote?${paramsString}`
|
||||
)
|
||||
|
||||
const res = await response.json()
|
||||
const data = res.data
|
||||
|
||||
return {
|
||||
routes: res.data as RouteInfo[],
|
||||
bestRoute: (data.length ? data[0] : null) as RouteInfo | null,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 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}/swap?${paramsString}`)
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleGetRoutes = async (
|
||||
inputMint = 'So11111111111111111111111111111111111111112',
|
||||
outputMint = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
|
||||
amount = 0,
|
||||
slippage = 50,
|
||||
swapMode = 'ExactIn',
|
||||
feeBps = 0,
|
||||
wallet: string | undefined | null
|
||||
) => {
|
||||
wallet ||= PublicKey.default.toBase58()
|
||||
|
||||
const results = await Promise.allSettled([
|
||||
fetchMangoRoutes(
|
||||
inputMint,
|
||||
outputMint,
|
||||
amount,
|
||||
slippage,
|
||||
swapMode,
|
||||
feeBps,
|
||||
wallet
|
||||
),
|
||||
fetchJupiterRoutes(
|
||||
inputMint,
|
||||
outputMint,
|
||||
amount,
|
||||
slippage,
|
||||
swapMode,
|
||||
feeBps
|
||||
),
|
||||
])
|
||||
const responses = results
|
||||
.filter((x) => x.status === 'fulfilled' && x.value.bestRoute !== null)
|
||||
.map((x) => (x as any).value)
|
||||
|
||||
const sortedByBiggestOutAmount = (
|
||||
responses as {
|
||||
routes: RouteInfo[]
|
||||
bestRoute: RouteInfo
|
||||
}[]
|
||||
).sort(
|
||||
(a, b) => Number(b.bestRoute.outAmount) - Number(a.bestRoute.outAmount)
|
||||
)
|
||||
|
||||
return {
|
||||
routes: sortedByBiggestOutAmount[0].routes,
|
||||
bestRoute: sortedByBiggestOutAmount[0].bestRoute,
|
||||
}
|
||||
}
|
||||
|
||||
const useQuoteRoutes = ({
|
||||
inputMint,
|
||||
outputMint,
|
||||
amount,
|
||||
slippage,
|
||||
swapMode,
|
||||
wallet,
|
||||
}: useQuoteRoutesPropTypes) => {
|
||||
const { inputTokenInfo, outputTokenInfo } = useJupiterSwapData()
|
||||
|
||||
const decimals =
|
||||
swapMode === 'ExactIn'
|
||||
? inputTokenInfo?.decimals || 6
|
||||
: outputTokenInfo?.decimals || 6
|
||||
|
||||
const nativeAmount =
|
||||
amount && !Number.isNaN(+amount)
|
||||
? new Decimal(amount).mul(10 ** decimals)
|
||||
: new Decimal(0)
|
||||
|
||||
const res = useQuery<{ routes: RouteInfo[]; bestRoute: RouteInfo }, Error>(
|
||||
['swap-routes', inputMint, outputMint, amount, slippage, swapMode, wallet],
|
||||
async () =>
|
||||
handleGetRoutes(
|
||||
inputMint,
|
||||
outputMint,
|
||||
nativeAmount.toNumber(),
|
||||
slippage,
|
||||
swapMode,
|
||||
0,
|
||||
wallet
|
||||
),
|
||||
{
|
||||
enabled: amount ? true : false,
|
||||
}
|
||||
)
|
||||
|
||||
return amount
|
||||
? {
|
||||
...(res.data ?? {
|
||||
routes: [],
|
||||
bestRoute: undefined,
|
||||
}),
|
||||
isLoading: res.isLoading,
|
||||
}
|
||||
: {
|
||||
routes: [],
|
||||
bestRoute: undefined,
|
||||
isLoading: false,
|
||||
}
|
||||
}
|
||||
|
||||
export default useQuoteRoutes
|
|
@ -1,13 +1,14 @@
|
|||
import { AccountMeta } from '@solana/web3.js'
|
||||
import { AccountInfo } from '@solana/web3.js'
|
||||
import { AccountInfo, PublicKey, TransactionInstruction } from '@solana/web3.js'
|
||||
import Decimal from 'decimal.js'
|
||||
|
||||
export declare type SideType = typeof Side.Ask | typeof Side.Bid
|
||||
export declare const Side: {
|
||||
Bid: {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
bid: {}
|
||||
}
|
||||
Ask: {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
ask: {}
|
||||
}
|
||||
}
|
||||
|
@ -57,7 +58,7 @@ export interface ExactOutSwapParams extends SwapParams {
|
|||
}
|
||||
export declare type AccountInfoMap = Map<string, AccountInfo<Buffer> | null>
|
||||
|
||||
declare type AmmLabel =
|
||||
export declare type AmmLabel =
|
||||
| 'Aldrin'
|
||||
| 'Crema'
|
||||
| 'Cropper'
|
||||
|
@ -127,6 +128,9 @@ export interface RouteInfo {
|
|||
priceImpactPct: number
|
||||
slippageBps: number
|
||||
swapMode: SwapMode
|
||||
instructions?: TransactionInstruction[]
|
||||
mints?: PublicKey[]
|
||||
routerName?: 'Mango'
|
||||
}
|
||||
|
||||
export type Routes = {
|
||||
|
|
|
@ -58,6 +58,8 @@ export const PROFILE_CATEGORIES = [
|
|||
|
||||
export const CHART_DATA_FEED = `https://dry-ravine-67635.herokuapp.com/tv`
|
||||
|
||||
export const MANGO_ROUTER_API_URL = 'https://api.mngo.cloud/router/v1'
|
||||
|
||||
export const DEFAULT_MARKET_NAME = 'SOL/USDC'
|
||||
|
||||
export const MIN_SOL_BALANCE = 0.001
|
||||
|
|
Loading…
Reference in New Issue