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' type UseJupiterProps = Parameters[0] const JupiterForm: FunctionComponent = () => { 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(true) 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(() => { 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 return (
{connected && walletTokensWithInfos.length && walletTokenPrices && !isMobile ? (
) : null}
{/*
Swap
refresh()}> setShowSettings(true)}>
*/}
{connected ? ( <> { setFormValue((val) => ({ ...val, amount: inputWalletBalance(), })) }} > Max ) : null}
{ let newValue = e.target?.value || 0 newValue = Number.isNaN(newValue) ? 0 : newValue setFormValue((val) => ({ ...val, amount: newValue, })) }} />
Bal: {outputWalletBalance()}
{selectedRoute?.outAmount && outputTokenPrice ? (
≈ $ {( (selectedRoute?.outAmount / 10 ** (outputTokenInfo?.decimals || 1)) * outputTokenPrice ).toFixed(2)}
) : null}
{routes?.length && selectedRoute ? (
{selectedRoute === routes[0] ? (
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 ? ' → ' : ''} ) })}
Swap Details
refresh()}> setShowSettings(true)}>
Rate
1 {outputTokenInfo?.symbol} ≈{' '} {(formValue?.amount / outAmountUi).toFixed(6)}{' '} {inputTokenInfo?.symbol}
Price Impact
{selectedRoute?.priceImpactPct * 100 < 0.1 ? '< 0.1%' : `~ ${( selectedRoute?.priceImpactPct * 100 ).toFixed(4)}%`}
Minimum Received
{selectedRoute?.outAmountWithSlippage / 10 ** (outputTokenInfo?.decimals || 1)}{' '} {outputTokenInfo?.symbol}
{!isNaN(feeValue) ? (
Swap Fee
≈ ${feeValue?.toFixed(2)}
{selectedRoute?.marketInfos.map( (info, index) => { const feeToken = tokens.find( (item) => item?.address === info.lpFee?.mint ) return (
Fees paid to{' '} {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 (
Fees paid to {info.marketMeta?.amm?.label}
{( info.lpFee?.amount / Math.pow(10, feeToken?.decimals) ).toFixed(6)}{' '} {feeToken?.symbol} ({info.lpFee?.pct * 100}%)
) }) )} {connected ? ( <>
Transaction Fee
{depositAndFee ? depositAndFee?.signatureFee / Math.pow(10, 9) : '-'}{' '} SOL
{depositAndFee?.ataDepositLength || depositAndFee?.openOrdersDeposits?.length ? (
Deposit {depositAndFee?.ataDepositLength ? (
You need to have an Associated Token Account.
) : null} {depositAndFee?.openOrdersDeposits ?.length ? (
Serum requires an OpenOrders account for each token. You can close the account and recover the SOL later.{' '} Here's how
) : null} } placement={'left'} >
{depositAndFee?.ataDepositLength ? (
{( depositAndFee?.ataDeposit / Math.pow(10, 9) ).toFixed(5)}{' '} SOL for {depositAndFee?.ataDepositLength}{' '} ATA Account
) : null} {depositAndFee?.openOrdersDeposits?.length ? (
{( sum(depositAndFee?.openOrdersDeposits) / Math.pow(10, 9) ).toFixed(5)}{' '} SOL for{' '} {depositAndFee?.openOrdersDeposits.length}{' '} Serum OpenOrders{' '} {depositAndFee?.openOrdersDeposits.length > 1 ? 'Accounts' : 'Account'}
) : null}
) : null} ) : null}
) : null} {error && (
Error in Jupiter – Try changing your input
)}
{showRoutesModal ? ( setShowRoutesModal(false)} >
{routes?.length} routes
{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)}> Before you get started...
Swaps interact directly with your connected wallet, not your Mango Account.
) : 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} {connected && !hasSwapped ? ( setHasSwapped(true)}> Before you get started...
Swaps interact directly with your connected wallet, not your Mango Account.
) : null}
{inputTokenInfo && outputTokenInfo ? ( ) : null}
) } export default JupiterForm