mango-v4-ui/components/swap/SwapReviewRouteInfo.tsx

1173 lines
40 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import {
TransactionInstruction,
PublicKey,
VersionedTransaction,
Connection,
TransactionMessage,
AddressLookupTableAccount,
} from '@solana/web3.js'
import Decimal from 'decimal.js'
import * as sentry from '@sentry/nextjs'
import mangoStore from '@store/mangoStore'
import Button, { IconButton } from '../shared/Button'
import Loading from '../shared/Loading'
import {
ArrowLeftIcon,
PencilIcon,
ArrowsRightLeftIcon,
ArrowRightIcon,
ChevronDownIcon,
ArrowPathIcon,
} from '@heroicons/react/20/solid'
import { useTranslation } from 'next-i18next'
import { formatNumericValue } from '../../utils/numbers'
import { notify } from '../../utils/notifications'
import useJupiterMints from '../../hooks/useJupiterMints'
import { JupiterV6RouteInfo, JupiterV6RoutePlan } from 'types/jupiter'
import useJupiterSwapData from './useJupiterSwapData'
// import { Transaction } from '@solana/web3.js'
import {
JUPITER_V6_QUOTE_API_MAINNET,
MANGO_ROUTER_API_URL,
SOUND_SETTINGS_KEY,
} from 'utils/constants'
import useLocalStorageState from 'hooks/useLocalStorageState'
import { Howl } from 'howler'
import { INITIAL_SOUND_SETTINGS } from '@components/settings/SoundSettings'
import Tooltip from '@components/shared/Tooltip'
import { Disclosure, Transition } from '@headlessui/react'
import RoutesModal from './RoutesModal'
// import { createAssociatedTokenAccountIdempotentInstruction } from '@blockworks-foundation/mango-v4'
import FormatNumericValue from '@components/shared/FormatNumericValue'
import { isMangoError } from 'types'
import { useWallet } from '@solana/wallet-adapter-react'
import TokenLogo from '@components/shared/TokenLogo'
import {
Bank,
TransactionErrors,
parseTxForKnownErrors,
} from '@blockworks-foundation/mango-v4'
import CircularProgress from '@components/shared/CircularProgress'
import {
QueryObserverResult,
RefetchOptions,
RefetchQueryFilters,
} from '@tanstack/react-query'
import { isTokenInsured } from '@components/DepositForm'
import UninsuredNotification from '@components/shared/UninsuredNotification'
import { sendTxAndConfirm } from 'utils/governance/tools'
import useAnalytics from 'hooks/useAnalytics'
import Checkbox from '@components/forms/Checkbox'
type JupiterRouteInfoProps = {
amountIn: Decimal
loadingRoute: boolean
isWalletSwap?: boolean
onClose: () => void
onSuccess?: () => void
refetchRoute:
| (<TPageData>(
options?: (RefetchOptions & RefetchQueryFilters<TPageData>) | undefined,
) => Promise<
QueryObserverResult<{ bestRoute: JupiterV6RouteInfo | null }, Error>
>)
| undefined
routes: JupiterV6RouteInfo[] | undefined
selectedRoute: JupiterV6RouteInfo | undefined | null
setSelectedRoute: Dispatch<
SetStateAction<JupiterV6RouteInfo | undefined | null>
>
slippage: number
show: boolean
}
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 calculateOutFees = (
routePlan: JupiterV6RoutePlan[],
inputBank: Bank,
outputBank: Bank,
outputDecimals: number,
): [number, number] => {
let outFee = 0
for (let i = 0; i < routePlan.length; i++) {
const r = routePlan[i].swapInfo
const price = r.outAmount / r.inAmount
outFee *= price
if (r.feeMint === r.outputMint) {
outFee += r.feeAmount
} else {
outFee += r.feeAmount * price
}
}
const jupiterFee = outFee / 10 ** outputDecimals
const flashLoanSwapFeeRate = Math.max(
inputBank.flashLoanSwapFeeRate,
outputBank.flashLoanSwapFeeRate,
)
const mangoSwapFee = routePlan[0].swapInfo.inAmount * flashLoanSwapFeeRate
return [jupiterFee, mangoSwapFee]
}
// 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, []]
// }
/** Given a Jupiter route, fetch the transaction for the user to sign.
**This function should ONLY be used for wallet swaps* */
export const fetchJupiterWalletSwapTransaction = async (
selectedRoute: JupiterV6RouteInfo,
userPublicKey: PublicKey,
slippage: number,
origin?: 'mango' | 'jupiter' | 'raydium',
): Promise<VersionedTransaction> => {
// docs https://station.jup.ag/api-v6/post-swap
const params: {
quoteResponse: JupiterV6RouteInfo
userPublicKey: PublicKey
slippageBps: number
autoCreateOutAta?: boolean
wrapAndUnwrapSol?: boolean
} = {
// response from /quote api
quoteResponse: selectedRoute,
// user public key to be used for the swap
userPublicKey,
slippageBps: Math.ceil(slippage * 100),
}
if (origin === 'mango') {
params.autoCreateOutAta = true
params.wrapAndUnwrapSol = true
}
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(params),
},
)
).json()
const { swapTransaction } = transactions
const parsedSwapTransaction = VersionedTransaction.deserialize(
Buffer.from(swapTransaction, 'base64'),
)
return parsedSwapTransaction
}
/** Given a Jupiter route, fetch the transaction for the user to sign.
**This function should be used for margin swaps* */
export 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 EMPTY_COINGECKO_PRICES = {
inputCoingeckoPrice: 0,
outputCoingeckoPrice: 0,
}
const successSound = new Howl({
src: ['/sounds/swap-success.mp3'],
volume: 0.5,
})
const SwapReviewRouteInfo = ({
amountIn,
loadingRoute,
isWalletSwap,
onClose,
onSuccess,
refetchRoute,
routes,
selectedRoute,
setSelectedRoute,
show,
}: 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)
const [feeValue] = useState<number | null>(null)
const [submitting, setSubmitting] = useState(false)
const [coingeckoPrices, setCoingeckoPrices] = useState(EMPTY_COINGECKO_PRICES)
const [acceptPriceImpact, setAcceptPriceImpact] = useState(false)
const { jupiterTokens } = useJupiterMints()
const { inputTokenInfo, outputTokenInfo } = useJupiterSwapData()
const inputBank = mangoStore((s) => s.swap.inputBank)
const outputBank = mangoStore((s) => s.swap.outputBank)
const [soundSettings] = useLocalStorageState(
SOUND_SETTINGS_KEY,
INITIAL_SOUND_SETTINGS,
)
const focusRef = useRef<HTMLButtonElement>(null)
const { sendAnalytics } = useAnalytics()
const [refetchRoutePercentage, setRefetchRoutePercentage] = useState(0)
useEffect(() => {
let currentPercentage = 0
const countdownInterval = setInterval(() => {
if (currentPercentage < 100) {
currentPercentage += 5 // 5% increment per second
setRefetchRoutePercentage(currentPercentage)
}
}, 1000)
return () => {
clearInterval(countdownInterval)
}
}, [selectedRoute])
const amountOut = useMemo(() => {
if (!selectedRoute?.outAmount || !outputTokenInfo) return
return new Decimal(selectedRoute.outAmount.toString()).div(
10 ** outputTokenInfo.decimals,
)
}, [selectedRoute, outputTokenInfo])
useEffect(() => {
if (focusRef?.current) {
focusRef.current.focus()
}
}, [focusRef])
useEffect(() => {
setCoingeckoPrices(EMPTY_COINGECKO_PRICES)
const fetchTokenPrices = async () => {
const inputId = inputTokenInfo?.extensions?.coingeckoId
const outputId = outputTokenInfo?.extensions?.coingeckoId
if (inputId && outputId) {
try {
const results = await fetch(
`https://api.coingecko.com/api/v3/simple/price?ids=${inputId},${outputId}&vs_currencies=usd`,
)
const json = await results.json()
if (json[inputId]?.usd && json[outputId]?.usd) {
setCoingeckoPrices({
inputCoingeckoPrice: json[inputId].usd,
outputCoingeckoPrice: json[outputId].usd,
})
}
} catch (e) {
console.error('Loading coingecko prices: ', e)
}
}
}
if (inputTokenInfo && outputTokenInfo) {
fetchTokenPrices()
}
}, [inputTokenInfo, outputTokenInfo])
const onWalletSwap = useCallback(async () => {
if (!selectedRoute || !inputBank || !outputBank || !wallet.publicKey) return
const actions = mangoStore.getState().actions
const client = mangoStore.getState().client
const set = mangoStore.getState().set
const connection = mangoStore.getState().connection
setSubmitting(true)
try {
const vtx = await fetchJupiterWalletSwapTransaction(
selectedRoute,
wallet.publicKey,
slippage,
selectedRoute.origin,
)
const latestBlockhash = await connection.getLatestBlockhash()
const sign = wallet.signTransaction!
const signed = await sign(vtx)
const txid = await sendTxAndConfirm(
client.opts.multipleConnections,
connection,
signed,
latestBlockhash,
)
set((s) => {
s.swap.amountIn = ''
s.swap.amountOut = ''
})
notify({
title: 'Transaction confirmed',
type: 'success',
txid,
})
actions.fetchWalletTokens(wallet.publicKey)
} catch (e) {
console.log('error swapping wallet tokens', e)
} finally {
setSubmitting(false)
onClose()
}
}, [
inputBank,
outputBank,
onClose,
selectedRoute,
slippage,
wallet.publicKey,
])
const onSwap = useCallback(async () => {
if (!selectedRoute) return
let directRouteFallbackUsed = false
try {
const client = mangoStore.getState().client
const group = mangoStore.getState().group
const actions = mangoStore.getState().actions
const set = mangoStore.getState().set
const mangoAccount = mangoStore.getState().mangoAccount.current
const inputBank = mangoStore.getState().swap.inputBank
const outputBank = mangoStore.getState().swap.outputBank
const connection = mangoStore.getState().connection
if (
!mangoAccount ||
!group ||
!inputBank ||
!outputBank ||
!wallet.publicKey
)
return
setSubmitting(true)
let tx = ''
const [ixs, alts] =
// selectedRoute.routerName === 'Mango'
// ? await prepareMangoRouterInstructions(
// selectedRoute,
// inputBank.mint,
// outputBank.mint,
// mangoAccount.owner,
// )
// :
selectedRoute.instructions
? [selectedRoute.instructions, []]
: await fetchJupiterTransaction(
connection,
selectedRoute,
wallet.publicKey,
slippage,
inputBank.mint,
outputBank.mint,
selectedRoute.origin,
)
try {
sendAnalytics(
{
inputMintPk: inputBank.mint,
amountIn: amountIn.toNumber(),
outputMintPk: outputBank.mint,
},
'swapping',
)
const { signature, slot } = await client.marginTrade({
group,
mangoAccount,
inputMintPk: inputBank.mint,
amountIn: amountIn.toNumber(),
outputMintPk: outputBank.mint,
userDefinedInstructions: ixs,
userDefinedAlts: alts,
flashLoanType: { swap: {} },
sequenceCheck: false,
})
tx = signature
set((s) => {
s.successAnimation.swap = true
s.swap.amountIn = ''
s.swap.amountOut = ''
})
if (soundSettings['swap-success']) {
successSound.play()
}
sendAnalytics(
{
tx: `${tx}`,
},
'swapSuccess',
)
notify({
title: 'Transaction confirmed',
type: 'success',
txid: signature,
noSound: true,
})
actions.fetchGroup()
actions.fetchSwapHistory(mangoAccount.publicKey.toString(), 30000)
await actions.reloadMangoAccount(slot)
if (onSuccess) {
onSuccess()
}
} catch (e) {
sendAnalytics(
{
e: `${e}`,
tx: `${tx}`,
},
'onSwapError',
)
console.error('onSwap error: ', e)
sentry.captureException(e)
if (isMangoError(e)) {
const slippageExceeded = await parseTxForKnownErrors(
connection,
e?.txid,
)
if (
slippageExceeded ===
TransactionErrors.JupiterSlippageToleranceExceeded
) {
notify({
title: t('swap:error-slippage-exceeded'),
description: t('swap:error-slippage-exceeded-desc'),
txid: e?.txid,
type: 'error',
})
} else {
notify({
title: 'Transaction failed',
description: e.message,
txid: e?.txid,
type: 'error',
})
}
} else {
const stringError = `${e}`
if (
stringError.toLowerCase().includes('max accounts') &&
routes?.length &&
routes.length > 1
) {
directRouteFallbackUsed = true
setSelectedRoute(
routes.filter(
(x) =>
JSON.stringify(x.routePlan) !==
JSON.stringify(selectedRoute.routePlan),
)[0],
)
notify({
title: 'Transaction failed',
description: `${stringError} - please review route and click swap again`,
type: 'error',
})
} else {
notify({
title: 'Transaction failed',
description: `${stringError} - please try again`,
type: 'error',
})
}
}
} finally {
setSubmitting(false)
}
} catch (e) {
console.error('Swap error:', e)
} finally {
if (!directRouteFallbackUsed) {
onClose()
}
}
}, [
sendAnalytics,
selectedRoute,
wallet.publicKey,
slippage,
amountIn,
soundSettings,
onSuccess,
t,
routes,
onClose,
])
const onClick = isWalletSwap ? onWalletSwap : onSwap
const [balance, borrowAmount] = useMemo(() => {
const mangoAccount = mangoStore.getState().mangoAccount.current
const inputBank = mangoStore.getState().swap.inputBank
if (!mangoAccount || !inputBank) return [0, 0]
const balance = mangoAccount.getTokenDepositsUi(inputBank)
const remainingBalance = balance - amountIn.toNumber()
const borrowAmount = remainingBalance < 0 ? Math.abs(remainingBalance) : 0
return [balance, borrowAmount]
}, [amountIn])
const [jupiterFees] = useMemo(() => {
if (
!selectedRoute?.routePlan ||
!inputBank ||
!outputBank ||
!outputTokenInfo
) {
return [0, 0]
}
return calculateOutFees(
selectedRoute?.routePlan,
inputBank,
outputBank,
outputTokenInfo.decimals,
)
}, [inputBank, outputBank, outputTokenInfo, selectedRoute])
const flashLoanFee = useMemo(() => {
if (!inputBank || !outputBank) return 0
const rate = Math.max(
inputBank.flashLoanSwapFeeRate,
outputBank.flashLoanSwapFeeRate,
)
return amountIn.mul(rate).toNumber()
}, [amountIn, inputBank, outputBank])
const coinGeckoPriceDifference = useMemo(() => {
if (
!coingeckoPrices?.outputCoingeckoPrice ||
!coingeckoPrices?.inputCoingeckoPrice ||
!amountOut
)
return
return amountIn
.div(amountOut)
.minus(
new Decimal(coingeckoPrices.outputCoingeckoPrice).div(
coingeckoPrices.inputCoingeckoPrice,
),
)
.div(amountIn.div(amountOut))
.mul(100)
}, [coingeckoPrices, amountIn, amountOut])
const isInsured = useMemo(() => {
const group = mangoStore.getState().group
return isTokenInsured(outputBank, group)
}, [outputBank])
const showPriceImpactCheck = useMemo(() => {
if (!coinGeckoPriceDifference) return true
return coinGeckoPriceDifference.gte(5)
}, [coinGeckoPriceDifference])
return routes?.length &&
selectedRoute &&
inputTokenInfo &&
outputTokenInfo &&
amountOut ? (
<Transition
className="absolute right-0 top-0 z-20 h-full w-full bg-th-bkg-1 pb-0"
show={show}
enter="transition ease-in duration-300"
enterFrom="-translate-x-full"
enterTo="translate-x-0"
leave="transition ease-out duration-300"
leaveFrom="translate-x-0"
leaveTo="-translate-x-full"
>
<div className="thin-scroll flex h-full flex-col justify-between overflow-y-auto">
<div>
<div className="relative w-full px-4 pt-4">
<IconButton
className="absolute text-th-fgd-2"
onClick={onClose}
size="small"
ref={focusRef}
>
<ArrowLeftIcon className="h-5 w-5" />
</IconButton>
<div className="absolute right-4 h-8 w-8">
<CircularProgress
size={32}
indicatorWidth={1}
trackWidth={1}
progress={refetchRoutePercentage}
/>
{refetchRoute ? (
<IconButton
className="absolute inset-0 text-th-fgd-2"
hideBg
onClick={() => refetchRoute()}
size="small"
ref={focusRef}
>
<ArrowPathIcon
className={`h-5 w-5 ${
loadingRoute ? 'animate-spin' : null
}`}
/>
</IconButton>
) : null}
</div>
</div>
<div className="flex justify-center bg-th-bkg-1 px-6 pt-4">
<div className="flex w-full flex-col items-center pb-4">
<div className="relative mb-2 w-[72px]">
<TokenLogo bank={inputBank} size={40} />
<div className="absolute right-0 top-0">
<TokenLogo bank={outputBank} size={40} />
</div>
</div>
<div className="mb-0.5 flex items-center text-center text-lg">
<span>
<span className="font-mono text-th-fgd-1">
<FormatNumericValue value={amountIn} />
</span>{' '}
<span className="text-xs text-th-fgd-4">
{inputBank?.name}
</span>
</span>
<ArrowRightIcon className="mx-2 h-5 w-5 text-th-fgd-2" />
<span>
<span className="font-mono text-th-fgd-1">
<FormatNumericValue value={amountOut} />
</span>{' '}
<span className="text-xs text-th-fgd-4">
{outputBank?.name}
</span>
</span>
</div>
</div>
</div>
<div className="space-y-2 overflow-auto px-6">
<div className="flex justify-between">
<p className="text-sm text-th-fgd-3">{t('swap:rate')}</p>
<div>
<div className="flex items-center justify-end">
<p className="text-right font-mono text-sm text-th-fgd-2">
{swapRate ? (
<>
1{' '}
<span className="font-body text-th-fgd-3">
{inputTokenInfo?.symbol} {' '}
</span>
<FormatNumericValue value={amountOut.div(amountIn)} />{' '}
<span className="font-body text-th-fgd-3">
{outputTokenInfo?.symbol}
</span>
</>
) : (
<>
1{' '}
<span className="font-body text-th-fgd-3">
{outputTokenInfo?.symbol} {' '}
</span>
<FormatNumericValue value={amountIn.div(amountOut)} />{' '}
<span className="font-body text-th-fgd-3">
{inputTokenInfo?.symbol}
</span>
</>
)}
</p>
<ArrowsRightLeftIcon
className="ml-1 h-4 w-4 cursor-pointer text-th-fgd-2 hover:text-th-active"
onClick={() => setSwapRate(!swapRate)}
/>
</div>
<div className="space-y-2 px-1 text-xs">
{coinGeckoPriceDifference ? (
<div
className={`text-right font-mono ${
coinGeckoPriceDifference.gt(1)
? 'text-th-down'
: 'text-th-up'
}`}
>
{Decimal.abs(coinGeckoPriceDifference).toFixed(1)}%{' '}
<span className="font-body text-th-fgd-3">{`${
coinGeckoPriceDifference.lte(0)
? 'cheaper'
: 'more expensive'
} than CoinGecko`}</span>
</div>
) : null}
</div>
</div>
</div>
<div className="flex justify-between">
<p className="text-sm text-th-fgd-3">
{t('swap:minimum-received')}
</p>
{outputTokenInfo?.decimals && selectedRoute ? (
<p className="text-right font-mono text-sm text-th-fgd-2">
{selectedRoute.swapMode === 'ExactIn' ? (
<FormatNumericValue
value={
selectedRoute.otherAmountThreshold /
10 ** outputTokenInfo.decimals
}
decimals={outputTokenInfo.decimals}
/>
) : (
<FormatNumericValue
value={
selectedRoute.outAmount / 10 ** outputTokenInfo.decimals
}
decimals={outputTokenInfo.decimals}
/>
)}{' '}
<span className="font-body text-th-fgd-3">
{outputTokenInfo?.symbol}
</span>
</p>
) : null}
</div>
{selectedRoute?.swapMode === 'ExactOut' ? (
<div className="flex justify-between">
<p className="text-sm text-th-fgd-3">
{t('swap:maximum-cost')}
</p>
{inputTokenInfo?.decimals && selectedRoute ? (
<p className="text-right font-mono text-sm text-th-fgd-2">
<FormatNumericValue
value={
selectedRoute.otherAmountThreshold /
10 ** inputTokenInfo.decimals
}
decimals={inputTokenInfo.decimals}
/>{' '}
<span className="font-body text-th-fgd-3">
{inputTokenInfo?.symbol}
</span>
</p>
) : null}
</div>
) : null}
<div className="flex justify-between">
<Tooltip
content={
<>
<p>
The price impact is the difference observed between the
total value of the entry tokens swapped and the
destination tokens obtained.
</p>
<p className="mt-1">
The bigger the trade is, the bigger the price impact can
be.
</p>
</>
}
>
<p className="tooltip-underline">{t('swap:price-impact')}</p>
</Tooltip>
<p className="text-right font-mono text-sm text-th-fgd-2">
{selectedRoute?.priceImpactPct
? selectedRoute.priceImpactPct < 0.1
? '<0.1%'
: `${Number(selectedRoute.priceImpactPct).toFixed(2)}%`
: ''}
</p>
</div>
{!isWalletSwap ? (
<div className="flex justify-between">
{/* <Tooltip content={t('swap:tooltip-flash-loan-fee')}> */}
<p>{t('swap:flash-loan-fee')}</p>
{/* </Tooltip> */}
<p className="text-right font-mono text-sm text-th-fgd-2">
<FormatNumericValue
value={flashLoanFee}
decimals={inputBank?.mintDecimals}
/>{' '}
<span className="font-body text-th-fgd-3">
{inputBank?.name}
</span>
</p>
</div>
) : null}
<div className="flex justify-between">
<Tooltip
content={
<>
<p>
The fee displayed here is an estimate and is displayed in
destination tokens for convenience. Note that each leg of
the swap may collect its fee in different tokens, so fees
may vary.
</p>
</>
}
>
<p className="tooltip-underline">Jupiter Fees</p>
</Tooltip>
<p className="text-right font-mono text-sm text-th-fgd-2">
<FormatNumericValue
value={jupiterFees}
decimals={outputTokenInfo.decimals}
/>{' '}
<span className="font-body text-th-fgd-3">
{outputTokenInfo?.symbol}
</span>
</p>
</div>
{/* <div className="flex justify-between">
<p className="text-th-fgd-3">Mango Fees</p>
<p className="text-right font-mono text-sm text-th-fgd-2">
≈{' '}
<FormatNumericValue
value={mangoFees}
decimals={outputTokenInfo.decimals}
/>{' '}
<span className="font-body text-th-fgd-3">
{outputTokenInfo?.symbol}
</span>
</p>
</div> */}
{borrowAmount && inputBank ? (
<>
<div className="flex justify-between">
<Tooltip
content={
balance
? t('swap:tooltip-borrow-balance', {
balance: formatNumericValue(balance),
borrowAmount: formatNumericValue(borrowAmount),
token: inputTokenInfo?.symbol,
rate: formatNumericValue(
inputBank.getBorrowRateUi(),
2,
),
})
: t('swap:tooltip-borrow-no-balance', {
borrowAmount: formatNumericValue(borrowAmount),
token: inputTokenInfo?.symbol,
rate: formatNumericValue(
inputBank.getBorrowRateUi(),
2,
),
})
}
delay={100}
>
<p className="tooltip-underline">{t('borrow-amount')}</p>
</Tooltip>
<p className="text-right font-mono text-sm text-th-fgd-2">
<FormatNumericValue value={borrowAmount} />{' '}
<span className="font-body text-th-fgd-3">
{inputTokenInfo?.symbol}
</span>
</p>
</div>
<div className="flex justify-between">
<Tooltip
content={t('loan-origination-fee-tooltip', {
fee: `${(
inputBank.loanOriginationFeeRate.toNumber() * 100
).toFixed(3)}%`,
})}
delay={100}
>
<p className="tooltip-underline">
{t('loan-origination-fee')}
</p>
</Tooltip>
<p className="text-right font-mono text-th-fgd-2">
<FormatNumericValue
value={
borrowAmount *
inputBank.loanOriginationFeeRate.toNumber()
}
decimals={inputBank.mintDecimals}
/>{' '}
<span className="font-body text-th-fgd-3">
{inputBank.name}
</span>
</p>
</div>
</>
) : null}
</div>
</div>
<div className="p-6">
{showPriceImpactCheck ? (
<div className="mb-4 rounded-md border border-th-error p-2">
<Checkbox
checked={acceptPriceImpact}
onChange={(e) => setAcceptPriceImpact(e.target.checked)}
>
<p className="whitespace-normal text-th-fgd-2">
{coinGeckoPriceDifference
? `I accept the rate for this swap is
${coinGeckoPriceDifference.toFixed(1)}% worse than Coingecko`
: 'I accept the price of this swap has not been benchmarked to Coingecko and could differ to current market prices'}
</p>
</Checkbox>
</div>
) : null}
<div className="mb-4 flex items-center justify-center">
<Button
onClick={onClick}
className="flex w-full items-center justify-center text-base"
size="large"
disabled={showPriceImpactCheck && !acceptPriceImpact}
>
{submitting ? (
<Loading className="mr-2 h-5 w-5" />
) : (
<div className="flex items-center">
<ArrowsRightLeftIcon className="mr-2 h-5 w-5" />
{t('swap')}
</div>
)}
</Button>
</div>
<div className="rounded-md bg-th-bkg-2">
<Disclosure>
{({ open }) => (
<>
<Disclosure.Button
className={`flex w-full items-center justify-between rounded-md p-3 focus-visible:bg-th-bkg-3 ${
open ? 'mb-2 rounded-b-none' : ''
}`}
>
<p>{t('swap:route-info')}</p>
<ChevronDownIcon
className={`${
open ? 'rotate-180' : 'rotate-0'
} h-5 w-5 text-th-fgd-3`}
/>
</Disclosure.Button>
<Disclosure.Panel className="space-y-2 p-3 pt-0">
<div className="flex items-center justify-between">
<p className="text-sm text-th-fgd-3">
{t('swap:swap-route')}
</p>
<div
className="flex items-center text-th-fgd-2 md:hover:cursor-pointer md:hover:text-th-fgd-3"
role="button"
onClick={() => setShowRoutesModal(true)}
>
<span className="overflow-ellipsis whitespace-nowrap">
{selectedRoute?.routePlan?.map((info, index) => {
let includeSeparator = false
if (
selectedRoute.routePlan &&
selectedRoute?.routePlan.length > 1 &&
index !== selectedRoute?.routePlan.length - 1
) {
includeSeparator = true
}
return (
<span key={index}>{`${info?.swapInfo.label} ${
includeSeparator ? 'x ' : ''
}`}</span>
)
})}
</span>
<PencilIcon className="ml-2 h-4 w-4 hover:text-th-active" />
</div>
</div>
{typeof feeValue === 'number' ? (
<div className="flex justify-between">
<p className="text-sm text-th-fgd-3">{t('fee')}</p>
<div className="flex items-center">
<p className="text-right font-mono text-sm text-th-fgd-2">
${feeValue?.toFixed(2)}
</p>
</div>
</div>
) : (
selectedRoute?.routePlan?.map((info, index) => {
const feeToken = jupiterTokens.find(
(item) => item?.address === info.swapInfo.feeMint,
)
return (
<div className="flex justify-between" key={index}>
<p className="text-sm text-th-fgd-3">
{t('swap:fees-paid-to', {
route: info?.swapInfo.label,
})}
</p>
{feeToken?.decimals && (
<p className="pl-4 text-right font-mono text-sm text-th-fgd-2">
{(
info.swapInfo.feeAmount /
Math.pow(10, feeToken.decimals)
).toFixed(6)}{' '}
<span className="font-body">
{feeToken?.symbol}
</span>{' '}
(
{(
(info.swapInfo.outputMint == feeToken.address
? info.swapInfo.feeAmount /
info.swapInfo.outAmount
: info.swapInfo.feeAmount /
info.swapInfo.inAmount) * 100
).toLocaleString(undefined, {
maximumSignificantDigits: 2,
})}
%)
</p>
)}
</div>
)
})
)}
</Disclosure.Panel>
</>
)}
</Disclosure>
</div>
{!isInsured ? (
<div className="mt-4">
<UninsuredNotification name={outputBank?.name} />
</div>
) : null}
</div>
{showRoutesModal ? (
<RoutesModal
show={showRoutesModal}
onClose={() => setShowRoutesModal(false)}
setSelectedRoute={setSelectedRoute}
selectedRoute={selectedRoute}
routes={routes}
inputTokenSymbol={inputTokenInfo?.name}
outputTokenInfo={outputTokenInfo}
/>
) : null}
</div>
</Transition>
) : null
}
export default React.memo(SwapReviewRouteInfo)