import { useEffect, useMemo, useState, FunctionComponent, useCallback, } from 'react' import { useJupiter, RouteInfo } from '@jup-ag/react-hook' import { TOKEN_LIST_URL } from '@jup-ag/core' import { PublicKey } from '@solana/web3.js' import useMangoStore from '../stores/useMangoStore' import { connectionSelector, walletConnectedSelector, walletSelector, } from '../stores/selectors' import { sortBy, sum } from 'lodash' import { CogIcon, ExclamationCircleIcon, ExternalLinkIcon, InformationCircleIcon, SwitchVerticalIcon, } from '@heroicons/react/outline' import { ChevronDownIcon } from '@heroicons/react/solid' import { abbreviateAddress } from '../utils' import SwapTokenSelect from './SwapTokenSelect' import { notify } from '../utils/notifications' import { Token } from '../@types/types' import { getTokenAccountsByOwnerWithWrappedSol, nativeToUi, zeroKey, } from '@blockworks-foundation/mango-client' import Button, { IconButton, LinkButton } from './Button' import { useViewport } from '../hooks/useViewport' import { breakpoints } from './TradePageGrid' import useLocalStorageState from '../hooks/useLocalStorageState' import Modal from './Modal' import { ElementTitle } from './styles' import { RefreshClockwiseIcon, WalletIcon } from './icons' import Tooltip from './Tooltip' import SwapSettingsModal from './SwapSettingsModal' import SwapTokenInfo from './SwapTokenInfo' import { numberFormatter } from './SwapTokenInfo' import { useTranslation } from 'next-i18next' type UseJupiterProps = Parameters[0] const JupiterForm: FunctionComponent = () => { const { t } = useTranslation(['common', 'swap']) const wallet = useMangoStore(walletSelector) const connection = useMangoStore(connectionSelector) const connected = useMangoStore(walletConnectedSelector) const [showSettings, setShowSettings] = useState(false) const [depositAndFee, setDepositAndFee] = useState(null) const [selectedRoute, setSelectedRoute] = useState(null) const [showInputTokenSelect, setShowInputTokenSelect] = useState(false) const [showOutputTokenSelect, setShowOutputTokenSelect] = useState(false) const [swapping, setSwapping] = useState(false) const [tokens, setTokens] = useState([]) const [outputTokenPrice, setOutputTokenPrice] = useState(null) const [coinGeckoList, setCoinGeckoList] = useState(null) const [walletTokens, setWalletTokens] = useState([]) const [slippage, setSlippage] = useState(0.5) const [formValue, setFormValue] = useState({ amount: null, inputMint: new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'), outputMint: new PublicKey('MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac'), slippage, }) const [hasSwapped, setHasSwapped] = useLocalStorageState('hasSwapped', false) const [showWalletDraw, setShowWalletDraw] = useState(false) const [walletTokenPrices, setWalletTokenPrices] = useState(null) const { width } = useViewport() const isMobile = width ? width < breakpoints.sm : false const [feeValue, setFeeValue] = useState(null) const [showRoutesModal, setShowRoutesModal] = useState(false) const fetchWalletTokens = useCallback(async () => { const ownedTokens = [] const ownedTokenAccounts = await getTokenAccountsByOwnerWithWrappedSol( connection, wallet.publicKey ) ownedTokenAccounts.forEach((account) => { const decimals = tokens.find( (t) => t?.address === account.mint.toString() )?.decimals const uiBalance = nativeToUi(account.amount, decimals || 6) ownedTokens.push({ account, uiBalance }) }) console.log('ownedToknes', ownedTokens) setWalletTokens(ownedTokens) }, [wallet, connection, tokens]) // @ts-ignore const [inputTokenInfo, outputTokenInfo] = useMemo(() => { return [ tokens.find( (item) => item?.address === formValue.inputMint?.toBase58() || '' ), tokens.find( (item) => item?.address === formValue.outputMint?.toBase58() || '' ), ] }, [ formValue.inputMint?.toBase58(), formValue.outputMint?.toBase58(), tokens, ]) useEffect(() => { if (width >= 1680) { setShowWalletDraw(true) } }, []) useEffect(() => { const fetchCoinGeckoList = async () => { const response = await fetch( 'https://api.coingecko.com/api/v3/coins/list' ) const data = await response.json() setCoinGeckoList(data) } fetchCoinGeckoList() }, []) useEffect(() => { if (connected) { fetchWalletTokens() } }, [connected]) useEffect(() => { if (!coinGeckoList?.length) return const fetchOutputTokenPrice = async () => { const id = coinGeckoList.find( (x) => x?.symbol?.toLowerCase() === outputTokenInfo?.symbol?.toLowerCase() )?.id if (id) { const results = await fetch( `https://api.coingecko.com/api/v3/simple/price?ids=${id}&vs_currencies=usd` ) const json = await results.json() if (json[id].usd) { setOutputTokenPrice(json[id].usd) } } } if (outputTokenInfo) { fetchOutputTokenPrice() } }, [outputTokenInfo, coinGeckoList]) const amountInDecimal = useMemo(() => { return formValue.amount * 10 ** (inputTokenInfo?.decimals || 1) }, [inputTokenInfo, formValue.amount]) const { routeMap, allTokenMints, routes, loading, exchange, error, refresh } = useJupiter({ ...formValue, amount: amountInDecimal, slippage: formValue.slippage, }) useEffect(() => { // Fetch token list from Jupiter API fetch(TOKEN_LIST_URL['mainnet-beta']) .then((response) => response.json()) .then((result) => { const tokens = allTokenMints.map((mint) => result.find((item) => item?.address === mint) ) setTokens(tokens) }) }, [allTokenMints]) useEffect(() => { if (routes) { setSelectedRoute(routes[0]) } }, [routes]) useEffect(() => { const getDepositAndFee = async () => { const fees = await selectedRoute.getDepositAndFee() setDepositAndFee(fees) } if (selectedRoute && connected) { getDepositAndFee() } }, [selectedRoute]) const outputTokenMints = useMemo(() => { if (routeMap.size && formValue.inputMint) { const routeOptions = routeMap.get(formValue.inputMint.toString()) const routeOptionTokens = routeOptions.map((address) => { return tokens.find((t) => { return t?.address === address }) }) return routeOptionTokens } else { return sortedTokenMints } }, [routeMap, tokens, formValue.inputMint]) const inputWalletBalance = () => { if (walletTokens.length) { const walletToken = walletTokens.filter((t) => { return t.account.mint.toString() === inputTokenInfo?.address }) const largestTokenAccount = sortBy(walletToken, 'uiBalance').reverse()[0] return largestTokenAccount?.uiBalance || 0.0 } return 0.0 } const outputWalletBalance = () => { if (walletTokens.length) { const walletToken = walletTokens.filter((t) => { return t.account.mint.toString() === outputTokenInfo?.address }) const largestTokenAccount = sortBy(walletToken, 'uiBalance').reverse()[0] return largestTokenAccount?.uiBalance || 0.0 } return 0.0 } const [walletTokensWithInfos] = useMemo(() => { const userTokens = [] tokens.map((item) => { const found = walletTokens.find( (token) => token.account.mint.toBase58() === item?.address ) if (found) { userTokens.push({ ...found, item }) } }) return [userTokens] }, [walletTokens, tokens]) const getWalletTokenPrices = async () => { const ids = walletTokensWithInfos.map( (token) => token.item.extensions.coingeckoId ) const response = await fetch( `https://api.coingecko.com/api/v3/simple/price?ids=${ids.toString()}&vs_currencies=usd` ) const data = await response.json() setWalletTokenPrices(data) } const getSwapFeeTokenValue = async () => { const mints = selectedRoute.marketInfos.map((info) => info.lpFee.mint) const response = await fetch( `https://api.coingecko.com/api/v3/simple/token_price/solana?contract_addresses=${mints.toString()}&vs_currencies=usd` ) const data = await response.json() const feeValue = selectedRoute.marketInfos.reduce((a, c) => { const feeToken = tokens.find((item) => item?.address === c.lpFee?.mint) const amount = c.lpFee?.amount / Math.pow(10, feeToken?.decimals) if (data[c.lpFee?.mint]) { return a + data[c.lpFee?.mint].usd * amount } if (c.lpFee?.mint === 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v') { return a + 1 * amount } }, 0) setFeeValue(feeValue) } useEffect(() => { if (selectedRoute) { getSwapFeeTokenValue() } }, [selectedRoute]) useEffect(() => { getWalletTokenPrices() }, [walletTokensWithInfos]) const handleSelectRoute = (route) => { setShowRoutesModal(false) setSelectedRoute(route) } const handleSwitchMints = () => { setFormValue((val) => ({ ...val, inputMint: formValue.outputMint, outputMint: formValue.inputMint, })) } const sortedTokenMints = sortBy(tokens, (token) => { return token?.symbol?.toLowerCase() }) const outAmountUi = selectedRoute ? selectedRoute.outAmount / 10 ** (outputTokenInfo?.decimals || 1) : null const swapDisabled = loading || !selectedRoute || routes?.length === 0 const inputTokenInfos = inputTokenInfo ? (inputTokenInfo as any) : null const outputTokenInfos = outputTokenInfo ? (outputTokenInfo as any) : null return (
{connected && walletTokensWithInfos.length && walletTokenPrices && !isMobile ? (
) : null}
{connected ? ( <> { setFormValue((val) => ({ ...val, amount: inputWalletBalance(), })) }} > {t('max')} ) : null}
{ let newValue = e.target?.value || 0 newValue = Number.isNaN(newValue) ? 0 : newValue setFormValue((val) => ({ ...val, amount: newValue, })) }} />
{t('swap:bal')} {outputWalletBalance()}
{selectedRoute?.outAmount && outputTokenPrice ? (
≈ $ {( (selectedRoute?.outAmount / 10 ** (outputTokenInfo?.decimals || 1)) * outputTokenPrice ).toFixed(2)}
) : null}
{routes?.length && selectedRoute ? (
{selectedRoute === routes[0] ? (
{t('swap:best-swap')}
) : null}
{selectedRoute?.marketInfos.map((info, index) => { let includeSeparator = false if ( selectedRoute?.marketInfos.length > 1 && index !== selectedRoute?.marketInfos.length - 1 ) { includeSeparator = true } return ( {`${ info.marketMeta.amm.label } ${includeSeparator ? 'x ' : ''}`} ) })}
{inputTokenInfo?.symbol} →{' '} {selectedRoute?.marketInfos.map((r, index) => { const showArrow = index !== selectedRoute?.marketInfos.length - 1 ? true : false return ( { tokens.find( (item) => item?.address === r?.outputMint?.toString() )?.symbol } {showArrow ? ' → ' : ''} ) })}
{t('swap:swap-details')}
refresh()}> setShowSettings(true)}>
{t('swap:rate')}
1 {outputTokenInfo?.symbol} ≈{' '} {numberFormatter.format( formValue?.amount / outAmountUi )}{' '} {inputTokenInfo?.symbol}
{Math.abs( ((formValue?.amount / outAmountUi - outputTokenPrice) / (formValue?.amount / outAmountUi)) * 100 ).toFixed(1)} %{' '} {`${ ((formValue?.amount / outAmountUi - outputTokenPrice) / (formValue?.amount / outAmountUi)) * 100 <= 0 ? 'cheaper' : 'more expensive' } than CoinGecko`}
{t('swap:price-impact')}
{selectedRoute?.priceImpactPct * 100 < 0.1 ? '< 0.1%' : `~ ${( selectedRoute?.priceImpactPct * 100 ).toFixed(4)}%`}
{t('swap:minimum-received')}
{numberFormatter.format( selectedRoute?.outAmountWithSlippage / 10 ** outputTokenInfo?.decimals || 1 )}{' '} {outputTokenInfo?.symbol}
{!isNaN(feeValue) ? (
{t('swap:swap-fee')}
≈ ${feeValue?.toFixed(2)}
{selectedRoute?.marketInfos.map( (info, index) => { const feeToken = tokens.find( (item) => item?.address === info.lpFee?.mint ) return (
{t('swap:fees-paid-to', { feeRecipient: info.marketMeta?.amm?.label, })}
{( info.lpFee?.amount / Math.pow(10, feeToken?.decimals) ).toFixed(6)}{' '} {feeToken?.symbol} ( {info.lpFee?.pct * 100} %)
) } )}
} placement={'left'} >
) : ( selectedRoute?.marketInfos.map((info, index) => { const feeToken = tokens.find( (item) => item?.address === info.lpFee?.mint ) return (
{t('swap:fees-paid-to', { feeRecipient: info.marketMeta?.amm?.label, })}
{( 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}
) : null} {error && (
{t('swap:jupiter-error')}
)}
{showRoutesModal ? ( setShowRoutesModal(false)} >
{t('swap:routes-found', { numberOfRoutes: routes?.length, })}
{routes.map((route, index) => { const selected = selectedRoute === route return (
) })}
) : null} {showInputTokenSelect ? ( setShowInputTokenSelect(false)} sortedTokenMints={sortedTokenMints} onTokenSelect={(token) => { setShowInputTokenSelect(false) setFormValue((val) => ({ ...val, inputMint: new PublicKey(token?.address), })) }} /> ) : null} {showOutputTokenSelect ? ( setShowOutputTokenSelect(false)} sortedTokenMints={outputTokenMints} onTokenSelect={(token) => { setShowOutputTokenSelect(false) setFormValue((val) => ({ ...val, outputMint: new PublicKey(token?.address), })) }} /> ) : null} {showSettings ? ( setShowSettings(false)} slippage={slippage} setSlippage={setSlippage} /> ) : null} {connected && !hasSwapped ? ( setHasSwapped(true)}> {t('swap:get-started')}
{t('swap-in-wallet')}
) : null} {showInputTokenSelect ? ( setShowInputTokenSelect(false)} sortedTokenMints={sortedTokenMints} onTokenSelect={(token) => { setShowInputTokenSelect(false) setFormValue((val) => ({ ...val, inputMint: new PublicKey(token?.address), })) }} /> ) : null} {showOutputTokenSelect ? ( setShowOutputTokenSelect(false)} sortedTokenMints={outputTokenMints} onTokenSelect={(token) => { setShowOutputTokenSelect(false) setFormValue((val) => ({ ...val, outputMint: new PublicKey(token?.address), })) }} /> ) : null}
{inputTokenInfo && outputTokenInfo ? ( ) : null}
) } export default JupiterForm