import React, { Dispatch, SetStateAction, useEffect, useMemo, useState, } from 'react' import { TransactionInstruction, PublicKey } from '@solana/web3.js' import { toUiDecimals } from '@blockworks-foundation/mango-v4' import { Jupiter, RouteInfo, TransactionFeeInfo } from '@jup-ag/core' import JSBI from 'jsbi' import Decimal from 'decimal.js' import mangoStore from '../../store/mangoStore' import RoutesModal from './RoutesModal' import Button, { IconButton } from '../shared/Button' import Loading from '../shared/Loading' import { ArrowLeftIcon, PencilIcon, SwitchHorizontalIcon, } from '@heroicons/react/solid' import { useTranslation } from 'next-i18next' import Image from 'next/image' import { floorToDecimal, formatDecimal, formatFixedDecimals, } from '../../utils/numbers' import { notify } from '../../utils/notifications' type JupiterRouteInfoProps = { amountIn: Decimal jupiter: Jupiter | undefined onClose: () => void routes: RouteInfo[] | undefined selectedRoute: RouteInfo | undefined setSelectedRoute: Dispatch> slippage: number } const parseJupiterRoute = async ( jupiter: Jupiter, selectedRoute: RouteInfo, userPublicKey: PublicKey ): Promise => { const { transactions } = await jupiter.exchange({ routeInfo: selectedRoute, userPublicKey, }) const { swapTransaction } = transactions const instructions = [] for (const ix of swapTransaction.instructions) { if ( ix.programId.toBase58() === 'JUP3c2Uh3WA4Ng34tw6kPd2G4C5BB21Xo36Je1s32Ph' ) { instructions.push(ix) } } return instructions } const EMPTY_COINGECKO_PRICES = { inputCoingeckoPrice: 0, outputCoingeckoPrice: 0, } const JupiterRouteInfo = ({ amountIn, onClose, jupiter, routes, selectedRoute, setSelectedRoute, }: JupiterRouteInfoProps) => { const { t } = useTranslation(['common', 'trade']) const [showRoutesModal, setShowRoutesModal] = useState(false) const [swapRate, setSwapRate] = useState(false) const [depositAndFee, setDepositAndFee] = useState() const [feeValue, setFeeValue] = useState(null) const [submitting, setSubmitting] = useState(false) const [coingeckoPrices, setCoingeckoPrices] = useState(EMPTY_COINGECKO_PRICES) const inputTokenInfo = mangoStore((s) => s.swap.inputTokenInfo) const outputTokenInfo = mangoStore((s) => s.swap.outputTokenInfo) const jupiterTokens = mangoStore((s) => s.jupiterTokens) const connected = mangoStore((s) => s.connected) const inputTokenIconUri = useMemo(() => { return inputTokenInfo ? inputTokenInfo.logoURI : '' }, [inputTokenInfo]) const amountOut = useMemo(() => { if (!selectedRoute || !outputTokenInfo) return return toUiDecimals( JSBI.toNumber(selectedRoute.outAmount), outputTokenInfo.decimals ) }, [selectedRoute, outputTokenInfo]) useEffect(() => { const getDepositAndFee = async () => { const fees = await selectedRoute?.getDepositAndFee() if (fees) { setDepositAndFee(fees) } } if (selectedRoute && connected) { getDepositAndFee() } }, [selectedRoute, connected]) useEffect(() => { setCoingeckoPrices(EMPTY_COINGECKO_PRICES) const fetchTokenPrices = async () => { const inputId = inputTokenInfo?.extensions?.coingeckoId const outputId = outputTokenInfo?.extensions?.coingeckoId if (inputId && outputId) { 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, }) } } } if (inputTokenInfo && outputTokenInfo) { fetchTokenPrices() } }, [inputTokenInfo, outputTokenInfo]) const onSwap = async () => { if (!jupiter || !selectedRoute) return try { const client = mangoStore.getState().client const group = mangoStore.getState().group const actions = mangoStore.getState().actions const mangoAccount = mangoStore.getState().mangoAccount.current const inputBank = mangoStore.getState().swap.inputBank const outputBank = mangoStore.getState().swap.outputBank if (!mangoAccount || !group || !inputBank || !outputBank) return const ixs = await parseJupiterRoute( jupiter, selectedRoute, mangoAccount!.owner ) try { setSubmitting(true) const tx = await client.marginTrade({ group, mangoAccount, inputMintPk: inputBank.mint, amountIn: amountIn.toNumber(), outputMintPk: outputBank.mint, userDefinedInstructions: ixs, flashLoanType: { swap: {} }, }) notify({ title: 'Transaction confirmed', type: 'success', txid: tx, }) await actions.reloadMangoAccount() } catch (e: any) { console.error('onSwap error: ', e) notify({ title: 'Transaction failed', description: e.message, txid: e?.signature, type: 'error', }) } finally { setSubmitting(false) } } catch (e) { console.error('Swap error:', e) } finally { onClose() } } const borrowAmount = useMemo(() => { const mangoAccount = mangoStore.getState().mangoAccount.current const inputBank = mangoStore.getState().swap.inputBank if (!mangoAccount || !inputBank) return 0 const remainingBalance = mangoAccount.getTokenDepositsUi(inputBank) - amountIn.toNumber() return remainingBalance < 0 ? Math.abs(remainingBalance) : 0 }, [amountIn]) const coinGeckoPriceDifference = useMemo(() => { return amountOut ? floorToDecimal( (amountIn.toNumber() / amountOut - coingeckoPrices?.outputCoingeckoPrice / coingeckoPrices?.inputCoingeckoPrice) / (amountIn.toNumber() / amountOut), 1 ) : 0 }, [coingeckoPrices, amountIn, amountOut]) return routes?.length && selectedRoute && outputTokenInfo && amountOut ? (

{`${amountIn} ${ inputTokenInfo!.symbol } for ${amountOut} ${outputTokenInfo.symbol}`}

Rate

{swapRate ? ( <> 1 {inputTokenInfo!.name} ≈{' '} {formatDecimal(amountOut / amountIn.toNumber(), 6)}{' '} {outputTokenInfo?.symbol} ) : ( <> 1 {outputTokenInfo?.symbol} ≈{' '} {formatDecimal(amountIn.toNumber() / amountOut, 6)}{' '} {inputTokenInfo!.name} )}

setSwapRate(!swapRate)} />
{coingeckoPrices?.outputCoingeckoPrice && coingeckoPrices?.inputCoingeckoPrice ? (
0 ? 'text-th-red' : 'text-th-green' }`} > {Math.abs(coinGeckoPriceDifference * 100).toFixed(1)}%{' '} {`${ coinGeckoPriceDifference * 100 <= 0 ? 'cheaper' : 'more expensive' } than CoinGecko`}
) : null}

{t('trade:minimum-received')}

{outputTokenInfo?.decimals ? (

{formatDecimal( JSBI.toNumber(selectedRoute?.otherAmountThreshold) / 10 ** outputTokenInfo.decimals || 1, 6 )}{' '} {outputTokenInfo?.symbol}

) : null}
{borrowAmount ? (

{t('borrow-amount')}

~ {formatFixedDecimals(borrowAmount)} {inputTokenInfo?.symbol}

) : null}

{t('trade:slippage')}

{selectedRoute?.priceImpactPct * 100 < 0.1 ? '< 0.1%' : `~ ${(selectedRoute?.priceImpactPct * 100).toFixed(4)}%`}

Swap Route

setShowRoutesModal(true)} > {selectedRoute?.marketInfos.map((info, index) => { let includeSeparator = false if ( selectedRoute?.marketInfos.length > 1 && index !== selectedRoute?.marketInfos.length - 1 ) { includeSeparator = true } return ( {`${info.amm.label} ${ includeSeparator ? 'x ' : '' }`} ) })}
{typeof feeValue === 'number' ? (

{t('fee')}

≈ ${feeValue?.toFixed(2)}

) : ( selectedRoute?.marketInfos.map((info, index) => { const feeToken = jupiterTokens.find( (item) => item?.address === info.lpFee?.mint ) return (

{t('trade:fees-paid-to', { route: info?.amm?.label, })}

{feeToken?.decimals && (

{( JSBI.toNumber(info.lpFee?.amount) / Math.pow(10, feeToken.decimals) ).toFixed(6)}{' '} {feeToken?.symbol} ({info.lpFee?.pct * 100}%)

)}
) }) )} {/* {connected ? ( <>
{t('swap:transaction-fee')}
{depositAndFee ? depositAndFee?.signatureFee / Math.pow(10, 9) : '-'}{' '} SOL
{depositAndFee?.ataDepositLength || depositAndFee?.openOrdersDeposits?.length ? (
{t('deposit')} {depositAndFee?.ataDepositLength ? (
{t('need-ata-account')}
) : null} {depositAndFee?.openOrdersDeposits?.length ? (
{t('swap:serum-requires-openorders')}{' '} {t('swap:heres-how')}
) : null} } placement={'left'} >
{depositAndFee?.ataDepositLength ? (
{depositAndFee?.ataDepositLength === 1 ? t('swap:ata-deposit-details', { cost: ( depositAndFee?.ataDeposit / Math.pow(10, 9) ).toFixed(5), count: depositAndFee?.ataDepositLength, }) : t('swap:ata-deposit-details_plural', { cost: ( depositAndFee?.ataDeposit / Math.pow(10, 9) ).toFixed(5), count: depositAndFee?.ataDepositLength, })}
) : null} {depositAndFee?.openOrdersDeposits?.length ? (
{depositAndFee?.openOrdersDeposits.length > 1 ? t('swap:serum-details_plural', { cost: ( sum(depositAndFee?.openOrdersDeposits) / Math.pow(10, 9) ).toFixed(5), count: depositAndFee?.openOrdersDeposits.length, }) : t('swap:serum-details', { cost: ( sum(depositAndFee?.openOrdersDeposits) / Math.pow(10, 9) ).toFixed(5), count: depositAndFee?.openOrdersDeposits.length, })}
) : null}
) : null} ) : null} */}
{showRoutesModal ? ( setShowRoutesModal(false)} setSelectedRoute={setSelectedRoute} selectedRoute={selectedRoute} routes={routes} inputTokenSymbol={inputTokenInfo!.name} outputTokenInfo={outputTokenInfo} /> ) : null}
) : null } export default React.memo(JupiterRouteInfo)