import { memo, useMemo, useEffect, useRef, useState, ChangeEvent } from 'react' import { Token } from '../../types/jupiter' import mangoStore from '@store/mangoStore' import { IconButton } from '../shared/Button' import { MagnifyingGlassIcon, XMarkIcon } from '@heroicons/react/20/solid' import { useTranslation } from 'next-i18next' import Decimal from 'decimal.js' import { getTokenInMax } from './useTokenMax' import useMangoAccount from 'hooks/useMangoAccount' import useJupiterMints from 'hooks/useJupiterMints' import useMangoGroup from 'hooks/useMangoGroup' import { PublicKey } from '@solana/web3.js' import FormatNumericValue from '@components/shared/FormatNumericValue' import { formatTokenSymbol } from 'utils/tokens' import TokenLogo from '@components/shared/TokenLogo' import Input from '@components/forms/Input' import { getInputTokenBalance } from './TriggerSwapForm' import { walletBalanceForToken } from '@components/DepositForm' import TokenReduceOnlyDesc from '@components/shared/TokenReduceOnlyDesc' export type SwapFormTokenListType = | 'input' | 'output' | 'reduce-input' | 'reduce-output' | 'wallet-input' | undefined const generateSearchTerm = (item: Token, searchValue: string) => { const normalizedSearchValue = searchValue.toLowerCase() const values = `${item.symbol} ${item.name}`.toLowerCase() const isMatchingWithSymbol = item.symbol.toLowerCase().indexOf(normalizedSearchValue) >= 0 const matchingSymbolPercent = isMatchingWithSymbol ? normalizedSearchValue.length / item.symbol.length : 0 return { token: item, matchingIdx: values.indexOf(normalizedSearchValue), matchingSymbolPercent, } } const startSearch = (items: Token[], searchValue: string) => { return items .map((item) => generateSearchTerm(item, searchValue)) .filter((item) => item.matchingIdx >= 0) .sort((i1, i2) => i1.matchingIdx - i2.matchingIdx) .sort((i1, i2) => i2.matchingSymbolPercent - i1.matchingSymbolPercent) .map((item) => item.token) } const TokenItem = ({ token, onSubmit, useMargin, type, }: { token: TokenInfoWithAmounts onSubmit: (x: string) => void useMargin: boolean type: SwapFormTokenListType }) => { const { t } = useTranslation('trade') const { address, symbol, name } = token const bank = useMemo(() => { const group = mangoStore.getState().group if (!group) return // if we want to let users swap from tokens not listed on Mango. // if (type === 'wallet-input') { // const hasBank = Array.from(group.banksMapByName.values()) // .map((b) => b[0].mint.toString()) // .find((mint) => mint === address) // if (hasBank) { // return group.getFirstBankByMint(new PublicKey(address)) // } // } return group.getFirstBankByMint(new PublicKey(address)) }, [address, type]) // const isReduceOnly = useMemo(() => { // if (!bank) return false // const borrowsReduceOnly = bank.areBorrowsReduceOnly() // const depositsReduceOnly = bank.areDepositsReduceOnly() // return borrowsReduceOnly && depositsReduceOnly // }, [bank]) return (
) } interface TokenInfoWithAmounts extends Token { amount?: Decimal amountWithBorrow?: Decimal } const SwapFormTokenList = ({ onClose, onTokenSelect, type, useMargin, }: { onClose: () => void onTokenSelect: (x: string) => void type: SwapFormTokenListType useMargin: boolean }) => { const { t } = useTranslation(['common', 'search', 'swap']) const [search, setSearch] = useState('') const { mangoTokens, jupiterTokens } = useJupiterMints() const inputBank = mangoStore((s) => s.swap.inputBank) const outputBank = mangoStore((s) => s.swap.outputBank) const walletTokens = mangoStore((s) => s.wallet.tokens) const { group } = useMangoGroup() const { mangoAccount, mangoAccountAddress } = useMangoAccount() const focusRef = useRef(null) useEffect(() => { function onEscape(e: KeyboardEvent) { if (e.keyCode === 27) { onClose() } } window.addEventListener('keydown', onEscape) return () => window.removeEventListener('keydown', onEscape) }, [onClose]) const tokenInfos: TokenInfoWithAmounts[] = useMemo(() => { if ( !mangoTokens.length || !group || !mangoAccount || !outputBank || !inputBank ) return [] if (type === 'input') { const filteredSortedTokens = mangoTokens .map((token) => { const tokenPk = new PublicKey(token.address) const tokenBank = group.getFirstBankByMint(tokenPk) const max = getTokenInMax( mangoAccount, tokenPk, outputBank.mint, group, useMargin, ) const price = tokenBank.uiPrice return { ...token, ...max, price } }) .filter((token) => (token.symbol === outputBank.name ? false : true)) .sort((a, b) => useMargin ? Number(b.amountWithBorrow.mul(b.price)) - Number(a.amountWithBorrow.mul(a.price)) : Number(b.amount.mul(b.price)) - Number(a.amount.mul(a.price)), ) return filteredSortedTokens } else if (type === 'reduce-input') { const filteredSortedTokens = mangoTokens .map((token) => { const tokenBank = group.getFirstBankByMint( new PublicKey(token.address), ) const uiAmount = mangoAccount.getTokenBalanceUi(tokenBank) const uiDollarValue = uiAmount * tokenBank.uiPrice return { ...token, amount: new Decimal(uiAmount), amountWithBorrow: new Decimal(uiAmount), absDollarValue: Math.abs(uiDollarValue), decimals: inputBank.mintDecimals, } }) .filter( (token) => token.symbol !== outputBank.name && token.absDollarValue > 0.0001, ) .sort((a, b) => b.absDollarValue - a.absDollarValue) return filteredSortedTokens } else if (type === 'wallet-input' && jupiterTokens.length) { // if we want to let users swap from tokens not listed on Mango. Some other changes are need to pass the mint to the swap function // const jupiterTokensWithAmount = [] // for (const token of jupiterTokens) { // const hasToken = walletTokens.find( // (t) => t.mint.toString() === token.address, // ) // if (hasToken) { // jupiterTokensWithAmount.push({ // ...token, // amount: new Decimal(hasToken.uiAmount), // }) // } // } // const filteredSortedTokens = jupiterTokensWithAmount // .filter((token) => // token.symbol !== outputBank.name && token.amount.gt(0) ? true : false, // ) // .sort((a, b) => Number(b.amount) - Number(a.amount)) const filteredSortedTokens = mangoTokens .map((token) => { const tokenBank = group.getFirstBankByMint( new PublicKey(token.address), ) const max = walletBalanceForToken(walletTokens, tokenBank.name) const price = tokenBank.uiPrice return { ...token, amount: new Decimal(max.maxAmount), decimals: max.maxDecimals, price, } }) .filter((token) => (token.symbol === outputBank.name ? false : true)) .sort( (a, b) => Number(b.amount.mul(b.price)) - Number(a.amount.mul(a.price)), ) return filteredSortedTokens } else if (mangoTokens.length) { const filteredTokens = mangoTokens .map((token) => ({ ...token, amount: new Decimal(0), amountWithBorrow: new Decimal(0), })) .filter((token) => (token.symbol === inputBank.name ? false : true)) .sort((a, b) => a.symbol.localeCompare(b.symbol)) return filteredTokens } else return [] }, [ mangoTokens, jupiterTokens, inputBank, outputBank, mangoAccount, group, useMargin, type, ]) const handleUpdateSearch = (e: ChangeEvent) => { setSearch(e.target.value) } const sortedTokens = search ? startSearch(tokenInfos, search) : tokenInfos useEffect(() => { if (focusRef?.current) { focusRef.current.focus() } }, [focusRef]) const listTitle = useMemo(() => { if (!type) return '' if (type === 'input' || type === 'wallet-input') { return t('swap:you-sell') } else if (type === 'output') { return t('swap:you-buy') } else if (type === 'reduce-input') { return t('swap:reduce-position') } else { if (!mangoAccountAddress || !inputBank) return '' const uiPos = getInputTokenBalance(inputBank) if (uiPos > 0) { return t('swap:reduce-position-buy') } else if (uiPos < 0) { return t('swap:reduce-position-sell') } } }, [inputBank, mangoAccountAddress, type]) return ( <>

{listTitle}

{t('token')}

{!type?.includes('output') ? (

{t('max')}

) : null}
{sortedTokens?.length ? ( sortedTokens.map((token) => ( )) ) : (

{t('search:no-results')}

)}
) } export default memo(SwapFormTokenList)