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 mangoStore from '@store/mangoStore' import Button, { IconButton } from '../shared/Button' import Loading from '../shared/Loading' import { ArrowLeftIcon, PencilIcon, ArrowsRightLeftIcon, ArrowRightIcon, ChevronDownIcon, } from '@heroicons/react/20/solid' import { useTranslation } from 'next-i18next' import Image from 'next/legacy/image' import { formatNumericValue } 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' import { 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 } from '@headlessui/react' import RoutesModal from './RoutesModal' import { createAssociatedTokenAccountIdempotentInstruction } from '@blockworks-foundation/mango-v4' import FormatNumericValue from '@components/shared/FormatNumericValue' import { isMangoError } from 'types' type JupiterRouteInfoProps = { amountIn: Decimal onClose: () => void routes: RouteInfo[] | undefined selectedRoute: RouteInfo | undefined | null setSelectedRoute: Dispatch> slippage: number } 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 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, userPublicKey: PublicKey, slippage: number, inputMint: PublicKey, outputMint: PublicKey ): Promise<[TransactionInstruction[], AddressLookupTableAccount[]]> => { const transactions = await ( await fetch('https://quote-api.jup.ag/v4/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, // 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 [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()) ) } 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, onClose, routes, selectedRoute, setSelectedRoute, }: JupiterRouteInfoProps) => { const { t } = useTranslation(['common', 'trade']) const slippage = mangoStore((s) => s.swap.slippage) 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 { jupiterTokens } = useJupiterMints() const { inputTokenInfo, outputTokenInfo } = useJupiterSwapData() const inputBank = mangoStore((s) => s.swap.inputBank) const [soundSettings] = useLocalStorageState( SOUND_SETTINGS_KEY, INITIAL_SOUND_SETTINGS ) const focusRef = useRef(null) 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(() => { 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 onSwap = useCallback(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 set = mangoStore.getState().set const connection = mangoStore.getState().connection if (!mangoAccount || !group || !inputBank || !outputBank) return setSubmitting(true) 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({ group, mangoAccount, inputMintPk: inputBank.mint, amountIn: amountIn.toNumber(), outputMintPk: outputBank.mint, userDefinedInstructions: ixs, userDefinedAlts: alts, flashLoanType: { swap: {} }, }) set((s) => { s.successAnimation.swap = true }) if (soundSettings['swap-success']) { successSound.play() } notify({ title: 'Transaction confirmed', type: 'success', txid: tx, noSound: true, }) actions.fetchGroup() actions.fetchSwapHistory(mangoAccount.publicKey.toString(), 30000) await actions.reloadMangoAccount() } catch (e) { console.error('onSwap error: ', e) if (isMangoError(e)) { notify({ title: 'Transaction failed', description: e.message, txid: e?.txid, type: 'error', }) } } finally { setSubmitting(false) } } catch (e) { console.error('Swap error:', e) } finally { onClose() } }, [amountIn, onClose, selectedRoute, soundSettings]) 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 coinGeckoPriceDifference = useMemo(() => { return amountOut?.toNumber() ? amountIn .div(amountOut) .minus( new Decimal(coingeckoPrices?.outputCoingeckoPrice).div( coingeckoPrices?.inputCoingeckoPrice ) ) .div(amountIn.div(amountOut)) .mul(100) : new Decimal(0) }, [coingeckoPrices, amountIn, amountOut]) return routes?.length && selectedRoute && inputTokenInfo && outputTokenInfo && amountOut ? (

{' '} {inputTokenInfo?.symbol} {' '} {`${outputTokenInfo?.symbol}`}

{t('price')}

{swapRate ? ( <> 1{' '} {inputTokenInfo?.symbol} ≈{' '} {' '} {outputTokenInfo?.symbol} ) : ( <> 1{' '} {outputTokenInfo?.symbol} ≈{' '} {' '} {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 && selectedRoute ? (

{selectedRoute.swapMode === 'ExactIn' ? ( ) : ( )}{' '} {outputTokenInfo?.symbol}

) : null}
{selectedRoute?.swapMode === 'ExactOut' ? (

{t('swap:maximum-cost')}

{inputTokenInfo?.decimals && selectedRoute ? (

{' '} {inputTokenInfo?.symbol}

) : null}
) : null}

The price impact is the difference observed between the total value of the entry tokens swapped and the destination tokens obtained.

The bigger the trade is, the bigger the price impact can be.

} > {t('swap:price-impact')}

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

{borrowAmount ? (

{t('borrow-amount')}

~{' '} {inputTokenInfo?.symbol}

) : null}
{({ open }) => ( <>

{open ? t('swap:hide-fees') : t('swap:show-fees')}

{borrowAmount ? (

{t('loan-origination-fee')}

~ {' '} {inputBank!.name}

) : null}

{t('swap: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 = jupiterTokens.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).toLocaleString( undefined, { maximumSignificantDigits: 2, } )} %)

)}
) }) )}
)}
{showRoutesModal ? ( setShowRoutesModal(false)} setSelectedRoute={setSelectedRoute} selectedRoute={selectedRoute} routes={routes} inputTokenSymbol={inputTokenInfo?.name} outputTokenInfo={outputTokenInfo} /> ) : null}
) : null } export default React.memo(SwapReviewRouteInfo)