import React, { Dispatch, SetStateAction, useCallback, useEffect, useMemo, 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 { 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' 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' type JupiterRouteInfoProps = { amountIn: Decimal onClose: () => void routes: RouteInfo[] | undefined selectedRoute: RouteInfo | undefined 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 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)) console.log('ixs: ', ixs) console.log('filtered ixs: ', filtered_jup_ixs) 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 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 = 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 const [ixs, alts] = await fetchJupiterTransaction( connection, selectedRoute, mangoAccount.owner, slippage, inputBank.mint, outputBank.mint ) try { setSubmitting(true) 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.swap.success = 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: any) { console.error('onSwap error: ', 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 && outputTokenInfo && amountOut ? (

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

{t('price')}

{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}

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

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

{borrowAmount ? (

{t('borrow-amount')}

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

) : 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 ' : '' }`} ) })}
{({ open }) => ( <>

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

{borrowAmount ? (

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

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

) : null} {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, { maximumFractionDigits: 4, } )} %)

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