import React, { Dispatch, SetStateAction, useEffect, useMemo, useState, } from 'react' import { TransactionInstruction, PublicKey } from '@solana/web3.js' 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, ArrowsRightLeftIcon, CheckCircleIcon, ArrowRightIcon, } from '@heroicons/react/20/solid' import { useTranslation } from 'next-i18next' import Image from 'next/legacy/image' import { floorToDecimal, formatDecimal, formatFixedDecimals, } from '../../utils/numbers' import { notify } from '../../utils/notifications' import useJupiterMints from '../../hooks/useJupiterMints' import { RouteInfo } from 'types/jupiter' import useJupiterSwapData from './useJupiterSwapData' import { Transaction } from '@solana/web3.js' type JupiterRouteInfoProps = { amountIn: Decimal onClose: () => void routes: RouteInfo[] | undefined selectedRoute: RouteInfo | undefined setSelectedRoute: Dispatch> slippage: number } const parseJupiterRoute = async ( selectedRoute: RouteInfo, userPublicKey: PublicKey, slippage: number ): Promise => { const transactions = await ( await fetch('https://quote-api.jup.ag/v3/swap', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ // route from /quote api route: selectedRoute, // user public key to be used for the swap userPublicKey, // auto wrap and unwrap SOL. default is true wrapUnwrapSOL: true, // feeAccount is optional. Use if you want to charge a fee. feeBps must have been passed in /quote API. // This is the ATA account for the output token where the fee will be sent to. If you are swapping from SOL->USDC then this would be the USDC ATA you want to collect the fee. // feeAccount: 'fee_account_public_key', slippageBps: Math.ceil(slippage * 100), }), }) ).json() const { swapTransaction } = transactions const parsedSwapTransaction = Transaction.from( Buffer.from(swapTransaction, 'base64') ) const instructions = [] for (const ix of parsedSwapTransaction.instructions) { if ( ix.programId.toBase58() === 'JUP4Fb2cqiRUcaTHdrPC8h2gNsA2ETXiPDD33WcGuJB' ) { instructions.push(ix) } } return instructions } const EMPTY_COINGECKO_PRICES = { inputCoingeckoPrice: 0, outputCoingeckoPrice: 0, } const JupiterRouteInfo = ({ amountIn, onClose, routes, selectedRoute, setSelectedRoute, }: JupiterRouteInfoProps) => { const { t } = useTranslation(['common', 'trade']) const [showRoutesModal, setShowRoutesModal] = useState(false) const [swapRate, setSwapRate] = useState(false) const [feeValue] = useState(null) const [submitting, setSubmitting] = useState(false) const [coingeckoPrices, setCoingeckoPrices] = useState(EMPTY_COINGECKO_PRICES) const { mangoTokens } = useJupiterMints() const { inputTokenInfo, outputTokenInfo } = useJupiterSwapData() const inputBank = mangoStore((s) => s.swap.inputBank) const inputTokenIconUri = useMemo(() => { return inputTokenInfo ? inputTokenInfo.logoURI : '' }, [inputTokenInfo]) const amountOut = useMemo(() => { if (!selectedRoute || !outputTokenInfo) return return new Decimal(selectedRoute.outAmount.toString()).div( 10 ** outputTokenInfo.decimals ) }, [selectedRoute, outputTokenInfo]) 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 (!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 const slippage = mangoStore.getState().swap.slippage const set = mangoStore.getState().set if (!mangoAccount || !group || !inputBank || !outputBank) return const ixs = await parseJupiterRoute( selectedRoute, mangoAccount.owner, slippage ) try { setSubmitting(true) const tx = await client.marginTrade({ group, mangoAccount, inputMintPk: inputBank.mint, amountIn: amountIn.toNumber(), outputMintPk: outputBank.mint, userDefinedInstructions: ixs, flashLoanType: { swap: {} }, }) set((s) => { s.swap.success = true }) notify({ title: 'Transaction confirmed', type: 'success', txid: tx, }) actions.fetchGroup() 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() const x = remainingBalance < 0 ? Math.abs(remainingBalance) : 0 console.log('borrowAmount', x) return x }, [amountIn]) const coinGeckoPriceDifference = useMemo(() => { return amountOut ? floorToDecimal( amountIn .div(amountOut) .minus( new Decimal(coingeckoPrices?.outputCoingeckoPrice).div( coingeckoPrices?.inputCoingeckoPrice ) ) .div(amountIn.div(amountOut)), 1 ) : new Decimal(0) }, [coingeckoPrices, amountIn, amountOut]) console.log('selectedRoute', selectedRoute) return routes?.length && selectedRoute && outputTokenInfo && amountOut ? (

{`${formatFixedDecimals( amountIn.toNumber() )}`}{' '} {inputTokenInfo!.symbol} {`${formatFixedDecimals( amountOut.toNumber() )}`}{' '} {`${outputTokenInfo.symbol}`}

{t('swap:rate')}

{swapRate ? ( <> 1{' '} {inputTokenInfo!.name} ≈{' '} {formatFixedDecimals(amountOut.div(amountIn).toNumber())}{' '} {outputTokenInfo?.symbol} ) : ( <> 1{' '} {outputTokenInfo?.symbol} ≈{' '} {formatFixedDecimals(amountIn.div(amountOut).toNumber())}{' '} {inputTokenInfo!.symbol} )}

setSwapRate(!swapRate)} />
{coingeckoPrices?.outputCoingeckoPrice && coingeckoPrices?.inputCoingeckoPrice ? (
{Decimal.abs(coinGeckoPriceDifference).toFixed(1)}%{' '} {`${ coinGeckoPriceDifference.lte(0) ? 'cheaper' : 'more expensive' } than CoinGecko`}
) : null}

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

{outputTokenInfo?.decimals ? (

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

) : null}
{borrowAmount ? ( <>

{t('borrow-amount')}

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

Borrow Fee

~{' '} {formatFixedDecimals( amountIn .mul(inputBank!.loanOriginationFeeRate.toFixed()) .toNumber() )}{' '} {inputBank!.name}

) : null}

Est. {t('swap:slippage')}

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

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?.label} ${ includeSeparator ? 'x ' : '' }`} ) })}
{typeof feeValue === 'number' ? (

{t('fee')}

≈ ${feeValue?.toFixed(2)}

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

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

{feeToken?.decimals && (

{( 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)