import { useEffect, useMemo, useState, FunctionComponent } 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 { actionsSelector, connectionSelector, walletConnectedSelector, walletSelector, } from '../stores/selectors' import { sortBy, sum } from 'lodash' import { ChevronDownIcon, ChevronUpIcon, SwitchVerticalIcon, } from '@heroicons/react/outline' import { sleep } from '../utils' import SwapTokenSelect from './SwapTokenSelect' import { notify } from '../utils/notifications' import { Token } from '../@types/types' import { zeroKey } from '@blockworks-foundation/mango-client' type UseJupiterProps = Parameters[0] const JupiterForm: FunctionComponent = () => { const wallet = useMangoStore(walletSelector) const connection = useMangoStore(connectionSelector) const connected = useMangoStore(walletConnectedSelector) const actions = useMangoStore(actionsSelector) const walletTokens = useMangoStore((s) => s.wallet.tokens) const [routesToShow, setRoutesToShow] = useState(2) const [maxHeight, setMaxHeight] = useState('170px') const [depositAndFee, setDepositAndFee] = useState(null) const [selectedRoute, setSelectedRoute] = useState(null) const [showInputTokenSelect, setShowInputTokenSelect] = useState(false) const [showOutputTokenSelect, setShowOutputTokenSelect] = useState(false) const [tokens, setTokens] = useState([]) const [inputTokenStats, setInputTokenStats] = useState(null) const [outputTokenStats, setOutputTokenStats] = useState(null) const [coinGeckoList, setCoinGeckoList] = useState(null) const [formValue, setFormValue] = useState({ amount: null, inputMint: new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'), outputMint: new PublicKey('MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac'), slippage: 0.5, }) // @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 (!coinGeckoList?.length) return const fetchInputTokenStats = async () => { const id = coinGeckoList.find( (x) => x?.symbol?.toLowerCase() === inputTokenInfo?.symbol?.toLowerCase() )?.id const results = await fetch( `https://api.coingecko.com/api/v3/coins/${id}/market_chart?vs_currency=usd&days=1` ) const json = await results.json() console.log('json i', json) setInputTokenStats(json) } const fetchOutputTokenStats = async () => { const id = coinGeckoList.find( (x) => x?.symbol?.toLowerCase() === outputTokenInfo?.symbol?.toLowerCase() )?.id const results = await fetch( `https://api.coingecko.com/api/v3/coins/${id}/market_chart?vs_currency=usd&days=1` ) const json = await results.json() console.log('json o', json) setOutputTokenStats(json) } if (inputTokenInfo) { fetchInputTokenStats() } if (outputTokenInfo) { fetchOutputTokenStats() } }, [inputTokenInfo, outputTokenInfo, coinGeckoList]) const amountInDecimal = useMemo(() => { return formValue.amount * 10 ** (inputTokenInfo?.decimals || 1) }, [inputTokenInfo, formValue.amount]) const { routeMap, allTokenMints, routes, loading, exchange, error } = 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 = useMemo(() => { if (walletTokens.length) { const walletToken = walletTokens.find((t) => { return t.account.mint.toString() === inputTokenInfo?.address }) return walletToken?.uiBalance } return 0.0 }, [inputTokenInfo]) const outputWalletBalance = useMemo(() => { if (walletTokens.length) { const walletToken = walletTokens.find((t) => { return t.account.mint.toString() === outputTokenInfo?.address }) return walletToken?.uiBalance } return 0.0 }, [outputTokenInfo]) const [inputTokenPrice, inputTokenChange] = useMemo(() => { if (inputTokenStats?.prices?.length) { const price = inputTokenStats.prices[inputTokenStats.prices.length - 1][1].toFixed(2) const change = ((inputTokenStats.prices[0][1] - inputTokenStats.prices[inputTokenStats.prices.length - 1][1]) / inputTokenStats.prices[0][1]) * 100 return [price, change] } return [0, 0] }, [inputTokenStats]) const [outputTokenPrice, outputTokenChange] = useMemo(() => { if (outputTokenStats?.prices?.length) { const price = outputTokenStats.prices[outputTokenStats.prices.length - 1][1].toFixed( 2 ) const change = ((outputTokenStats.prices[0][1] - outputTokenStats.prices[outputTokenStats.prices.length - 1][1]) / outputTokenStats.prices[0][1]) * 100 return [price, change] } return [0, 0] }, [outputTokenStats]) const handleSelectRoute = (route) => { setSelectedRoute(route) } const handleSwitchMints = () => { setFormValue((val) => ({ ...val, inputMint: formValue.outputMint, outputMint: formValue.inputMint, })) } const handleShowMore = () => { setRoutesToShow(routes.length) setMaxHeight('340px') } const handleShowLess = () => { setMaxHeight('160px') sleep(700).then(() => { setRoutesToShow(2) }) } const sortedTokenMints = sortBy(tokens, (token) => { return token?.symbol?.toLowerCase() }) const displayedRoutes = routes ? routes.slice(0, routesToShow) : [] const outAmountUi = selectedRoute ? selectedRoute.outAmount / 10 ** (outputTokenInfo?.decimals || 1) : null return (
{connected ? ( ) : null}
{ let newValue = Number(e.target?.value || 0) newValue = Number.isNaN(newValue) ? 0 : newValue setFormValue((val) => ({ ...val, amount: newValue ? Math.max(newValue, 0) : null, })) }} />
Balance {outputWalletBalance}
{routes ? (
{routes?.length} routes found!
{displayedRoutes.map((route, index) => { const selected = selectedRoute === route return (
) })}
{routes.length ? (
{routes.length > displayedRoutes.length ? (
) : (
)}
) : null} {routes.length ? (
from{' '} {routes[routes.length - 1].outAmount / 10 ** (outputTokenInfo?.decimals || 1)}{' '} to{' '} {routes[0].outAmount / 10 ** (outputTokenInfo?.decimals || 1)}
) : null}
) : null} {error &&
Error in Jupiter, try changing your input
}
{inputTokenStats?.prices?.length && outputTokenStats?.prices?.length ? ( <>
{inputTokenInfo?.logoURI ? ( {inputTokenInfo?.symbol} ) : null}
{inputTokenInfo?.name}
${inputTokenPrice}
{(inputTokenChange * -1).toFixed(2)}%
{outputTokenInfo?.logoURI ? ( {outputTokenInfo?.symbol} ) : null}
{outputTokenInfo?.name}
${outputTokenPrice}
{(outputTokenChange * -1).toFixed(2)}%
) : null} {selectedRoute ? (
Price Info
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) ).toFixed(6)}{' '} {outputTokenInfo?.symbol}
{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
Deposit
{depositAndFee?.ataDeposit / Math.pow(10, 9)} SOL for{' '} {depositAndFee?.ataDepositLength} ATA Account {depositAndFee?.openOrdersDeposits?.length ? ( {sum(depositAndFee?.openOrdersDeposits) / Math.pow(10, 9)}{' '} SOL for {depositAndFee?.openOrdersDeposits.length} Serum OpenOrders Account ) : null}
) : null}
) : 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}
) } export default JupiterForm