From 410c0e67e88644a963a5efb1befbf773622129b6 Mon Sep 17 00:00:00 2001 From: damnnou Date: Thu, 21 Mar 2024 12:10:20 +0300 Subject: [PATCH] add: showNativeToken flag to selector tokens --- src/components/common/TokenSelector/index.tsx | 449 +++++++++++------- .../create-pool/CreatePoolForm/index.tsx | 9 +- .../create-pool/SelectPair/index.tsx | 3 + .../modals/TokenSelectorModal/index.tsx | 102 ++-- src/components/swap/TokenCard/index.tsx | 3 + src/hooks/tokens/useAllTokens.ts | 92 ++-- 6 files changed, 417 insertions(+), 241 deletions(-) diff --git a/src/components/common/TokenSelector/index.tsx b/src/components/common/TokenSelector/index.tsx index 9419edb..31282dd 100644 --- a/src/components/common/TokenSelector/index.tsx +++ b/src/components/common/TokenSelector/index.tsx @@ -1,116 +1,175 @@ -import { TokenFieldsFragment } from '@/graphql/generated/graphql' -import { useAlgebraToken } from '@/hooks/common/useAlgebraToken' -import { useCurrency } from '@/hooks/common/useCurrency' -import useDebounce from '@/hooks/common/useDebounce' -import { useFuse } from '@/hooks/common/useFuse' -import { useAllTokens } from '@/hooks/tokens/useAllTokens' -import { useCallback, useEffect, useMemo, useState } from 'react' -import { FixedSizeList } from 'react-window' -import { Address, isAddress } from 'viem' -import { useAccount, useBalance } from 'wagmi' -import CurrencyLogo from '../CurrencyLogo' -import { ADDRESS_ZERO, Currency, ExtendedNative, Token } from '@cryptoalgebra/integral-sdk' -import { formatCurrency } from '@/utils/common/formatCurrency' -import { useTokensState } from '@/state/tokensStore' -import { Copy } from 'lucide-react' -import { cn } from '@/lib/utils' +import { TokenFieldsFragment } from '@/graphql/generated/graphql'; +import { useAlgebraToken } from '@/hooks/common/useAlgebraToken'; +import { useCurrency } from '@/hooks/common/useCurrency'; +import useDebounce from '@/hooks/common/useDebounce'; +import { useFuse } from '@/hooks/common/useFuse'; +import { useAllTokens } from '@/hooks/tokens/useAllTokens'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { FixedSizeList } from 'react-window'; +import { Address, isAddress } from 'viem'; +import { useAccount, useBalance } from 'wagmi'; +import CurrencyLogo from '../CurrencyLogo'; +import { + ADDRESS_ZERO, + Currency, + ExtendedNative, + Token, +} from '@cryptoalgebra/integral-sdk'; +import { formatCurrency } from '@/utils/common/formatCurrency'; +import { useTokensState } from '@/state/tokensStore'; +import { Copy } from 'lucide-react'; +import { cn } from '@/lib/utils'; const TokenSelectorView = { DEFAULT_LIST: 'DEFAULT_LIST', IMPORT_TOKEN: 'IMPORT_TOKEN', - NOT_FOUND: 'NOT_FOUND' -} - -type TokenSelectorViewType = typeof TokenSelectorView[keyof typeof TokenSelectorView] - -const Search = ({ data, onSearch }: { data: TokenFieldsFragment[], onSearch: (matchedTokens: TokenFieldsFragment[], importToken: Token | undefined) => void}) => { - - const [query, setQuery] = useState
(undefined) - const debouncedQuery = useDebounce(query, 200) - const tokenEntity = useAlgebraToken(debouncedQuery && isAddress(debouncedQuery) ? debouncedQuery : undefined) - - const fuseOptions = useMemo(() => ({ - keys: ['id', 'symbol', 'name'], - threshold: 0 - }), []) + NOT_FOUND: 'NOT_FOUND', +}; + +type TokenSelectorViewType = + (typeof TokenSelectorView)[keyof typeof TokenSelectorView]; + +const Search = ({ + data, + onSearch, +}: { + data: TokenFieldsFragment[]; + onSearch: ( + matchedTokens: TokenFieldsFragment[], + importToken: Token | undefined + ) => void; +}) => { + const [query, setQuery] = useState
(undefined); + const debouncedQuery = useDebounce(query, 200); + const tokenEntity = useAlgebraToken( + debouncedQuery && isAddress(debouncedQuery) ? debouncedQuery : undefined + ); + + const fuseOptions = useMemo( + () => ({ + keys: ['id', 'symbol', 'name'], + threshold: 0, + }), + [] + ); const { result, pattern, search } = useFuse({ data, - options: fuseOptions - }) + options: fuseOptions, + }); const handleInput = (input: string | undefined) => { - setQuery(input) - } + setQuery(input); + }; useEffect(() => { - search(query) - }, [query, search]) - - useEffect(() => { - onSearch(result, tokenEntity instanceof ExtendedNative ? undefined : tokenEntity) - }, [result, tokenEntity, pattern, onSearch]) - - return handleInput(e.target.value)} - /> - - -} - -const LoadingRow = () =>
+ search(query); + }, [query, search]); -const TokenRow = ({ account, token, onSelect, otherCurrency, style }: { token: TokenFieldsFragment, account: Address | undefined, onSelect: (currency: Currency) => void, otherCurrency: Currency | null | undefined, style: React.CSSProperties }) => { - - const currency = useCurrency(token.id as Address) + useEffect(() => { + onSearch( + result, + tokenEntity instanceof ExtendedNative ? undefined : tokenEntity + ); + }, [result, tokenEntity, pattern, onSearch]); + + return ( + handleInput(e.target.value)} + /> + ); +}; + +const LoadingRow = () => ( +
+); + +const TokenRow = ({ + account, + token, + onSelect, + otherCurrency, + style, +}: { + token: TokenFieldsFragment; + account: Address | undefined; + onSelect: (currency: Currency) => void; + otherCurrency: Currency | null | undefined; + style: React.CSSProperties; +}) => { + const currency = useCurrency(token.id as Address); const { data: balance, isLoading } = useBalance({ address: account, - token: token.id === ADDRESS_ZERO ? undefined : token.id as Address - }) + token: token.id === ADDRESS_ZERO ? undefined : (token.id as Address), + }); - const lock = otherCurrency?.isNative ? token.id === ADDRESS_ZERO : token.id.toLowerCase() === otherCurrency?.wrapped.address.toLowerCase() + const lock = otherCurrency?.isNative + ? token.id === ADDRESS_ZERO + : token.id.toLowerCase() === + otherCurrency?.wrapped.address.toLowerCase(); - const [isCopied, setIsCopied] = useState(false) + const [isCopied, setIsCopied] = useState(false); const handleCopy = (e: React.MouseEvent) => { - e.stopPropagation() + e.stopPropagation(); navigator.clipboard.writeText(token.id).then(() => { - setIsCopied(true) - setTimeout(() => setIsCopied(false), 3000) - }) - } - - return + +
{token.name}
+
-
-
{token.symbol}
- -
-
{token.name}
+ {isLoading + ? 'Loading...' + : balance + ? formatCurrency.format(Number(balance.formatted)) + : ''}
- -
- {isLoading ? 'Loading...' : balance ? formatCurrency.format(Number(balance.formatted)) : ''} -
- -} - -const ImportTokenRow = ({ token, onImport }: { token: Token, onImport: (token: Token) => void }) =>
-
+ + ); +}; + +const ImportTokenRow = ({ + token, + onImport, +}: { + token: Token; + onImport: (token: Token) => void; +}) => ( +
+
@@ -119,87 +178,145 @@ const ImportTokenRow = ({ token, onImport }: { token: Token, onImport: (token: T
{token.name}
- +
- - -export const TokenSelector = ({ onSelect, otherCurrency }: { onSelect: (currency: Currency) => void, otherCurrency: Currency | null | undefined }) => { - - const { address: account } = useAccount() - - const [selectorView, setSelectorView] = useState(TokenSelectorView.DEFAULT_LIST) - - const { actions: { importToken } } = useTokensState() - - const { tokens, isLoading } = useAllTokens() - - const [matchedTokens, setMatchedTokens] = useState([]) - const [tokenForImport, setTokenForImport] = useState() - - const filteredTokens = useMemo(() => matchedTokens.length ? matchedTokens : tokens, [tokens, matchedTokens]) - - const handleSearch = (matchedTokens: TokenFieldsFragment[], importToken: Token | undefined) => { +); + +export const TokenSelector = ({ + onSelect, + otherCurrency, + showNativeToken, +}: { + onSelect: (currency: Currency) => void; + otherCurrency: Currency | null | undefined; + showNativeToken?: boolean; +}) => { + const { address: account } = useAccount(); + + const [selectorView, setSelectorView] = useState( + TokenSelectorView.DEFAULT_LIST + ); + + const { + actions: { importToken }, + } = useTokensState(); + + const { tokens, isLoading } = useAllTokens(showNativeToken); + + const [matchedTokens, setMatchedTokens] = useState( + [] + ); + const [tokenForImport, setTokenForImport] = useState(); + + const filteredTokens = useMemo( + () => (matchedTokens.length ? matchedTokens : tokens), + [tokens, matchedTokens] + ); + + const handleSearch = ( + matchedTokens: TokenFieldsFragment[], + importToken: Token | undefined + ) => { if (matchedTokens.length) { - setMatchedTokens(matchedTokens) - setSelectorView(TokenSelectorView.DEFAULT_LIST) + setMatchedTokens(matchedTokens); + setSelectorView(TokenSelectorView.DEFAULT_LIST); } else if (importToken) { - setTokenForImport(importToken) - setSelectorView(TokenSelectorView.IMPORT_TOKEN) + setTokenForImport(importToken); + setSelectorView(TokenSelectorView.IMPORT_TOKEN); } else if (!isLoading) { - setSelectorView(TokenSelectorView.NOT_FOUND) + setSelectorView(TokenSelectorView.NOT_FOUND); } - } + }; const handleImport = (token: Token) => { - importToken(token.address as Address, token.symbol || 'Unknown', token.name || 'Unknown', token.decimals, token.chainId) - setSelectorView(TokenSelectorView.DEFAULT_LIST) - setTokenForImport(undefined) - } - - const Row = useCallback(({ data, index, style }: { data: TokenFieldsFragment[], index: number, style: React.CSSProperties }) => { - - const token = data[index] - - if (!token) return null - - return - - }, [account, onSelect, otherCurrency]) - - const itemKey = useCallback((index: number, data: TokenFieldsFragment[]) => { - const currency = data[index] - return currency.name - }, []) - - return
- - { - selectorView === TokenSelectorView.DEFAULT_LIST - ? isLoading - ? - {LoadingRow} - - : - - {Row} - - : selectorView === TokenSelectorView.IMPORT_TOKEN && tokenForImport - ? - :
Token not found
- } + importToken( + token.address as Address, + token.symbol || 'Unknown', + token.name || 'Unknown', + token.decimals, + token.chainId + ); + setSelectorView(TokenSelectorView.DEFAULT_LIST); + setTokenForImport(undefined); + }; + + const Row = useCallback( + ({ + data, + index, + style, + }: { + data: TokenFieldsFragment[]; + index: number; + style: React.CSSProperties; + }) => { + const token = data[index]; + + if (!token) return null; + + return ( + + ); + }, + [account, onSelect, otherCurrency] + ); + + const itemKey = useCallback( + (index: number, data: TokenFieldsFragment[]) => { + const currency = data[index]; + return currency.name; + }, + [] + ); + + return ( +
+ + {selectorView === TokenSelectorView.DEFAULT_LIST ? ( + isLoading ? ( + + {LoadingRow} + + ) : ( + + {Row} + + ) + ) : selectorView === TokenSelectorView.IMPORT_TOKEN && + tokenForImport ? ( + + ) : ( +
+ Token not found +
+ )}
- -} \ No newline at end of file + ); +}; diff --git a/src/components/create-pool/CreatePoolForm/index.tsx b/src/components/create-pool/CreatePoolForm/index.tsx index 9243663..c813eaf 100644 --- a/src/components/create-pool/CreatePoolForm/index.tsx +++ b/src/components/create-pool/CreatePoolForm/index.tsx @@ -24,8 +24,11 @@ const CreatePoolForm = () => { const currencyA = currencies[SwapField.INPUT]; const currencyB = currencies[SwapField.OUTPUT]; + const isSameTokens = + currencyA?.wrapped.address === currencyB?.wrapped.address; + const poolAddress = - currencyA && currencyB + currencyA && currencyB && !isSameTokens ? (computePoolAddress({ tokenA: currencyA.wrapped, tokenB: currencyB.wrapped, @@ -96,7 +99,9 @@ const CreatePoolForm = () => { + + + + ); +}; - return - - {children} - - setIsOpen(false)} onEscapeKeyDown={() => setIsOpen(false)}> - - Select a token - - - - - - - - - - -} - -export default TokenSelectorModal \ No newline at end of file +export default TokenSelectorModal; diff --git a/src/components/swap/TokenCard/index.tsx b/src/components/swap/TokenCard/index.tsx index 85e3bc3..9053ff8 100644 --- a/src/components/swap/TokenCard/index.tsx +++ b/src/components/swap/TokenCard/index.tsx @@ -19,6 +19,7 @@ interface TokenSwapCardProps { priceImpact?: Percent; showMaxButton?: boolean; showBalance?: boolean; + showNativeToken?: boolean; disabled?: boolean; } @@ -32,6 +33,7 @@ const TokenCard = ({ fiatValue, showMaxButton, showBalance, + showNativeToken, disabled, }: TokenSwapCardProps) => { const [isOpen, setIsOpen] = useState(false); @@ -66,6 +68,7 @@ const TokenCard = ({
[], []) + const tokensBlackList: Address[] = useMemo(() => [], []); const mergedTokens = useMemo(() => { + if (!allTokens) return []; + + const tokens = new Map(); + + if (showNativeToken) + tokens.set(ADDRESS_ZERO, { + id: ADDRESS_ZERO, + symbol: DEFAULT_NATIVE_SYMBOL, + name: DEFAULT_NATIVE_NAME, + decimals: 18, + derivedMatic: 1, + }); + + for (const token of allTokens.tokens.filter( + (token) => !tokensBlackList.includes(token.id as Address) + )) { + tokens.set(token.id.toLowerCase() as Address, { ...token }); + } - if (!allTokens) return [] - - const tokens = new Map() - - tokens.set(ADDRESS_ZERO, { - id: ADDRESS_ZERO, - symbol: DEFAULT_NATIVE_SYMBOL, - name: DEFAULT_NATIVE_NAME, - decimals: 18, - derivedMatic: 1 - }) - - for (const token of allTokens.tokens.filter(token => !tokensBlackList.includes(token.id as Address))) { - tokens.set(token.id.toLowerCase() as Address, { ...token }) - } - - const _importedTokens = Object.values(importedTokens[chainId] || []) + const _importedTokens = Object.values(importedTokens[chainId] || []); for (const token of _importedTokens) { tokens.set(token.id.toLowerCase() as Address, { ...token, - derivedMatic: 0 - }) + derivedMatic: 0, + }); } - return [...tokens].map(([,token]) => ({...token})) - - }, [allTokens, importedTokens, tokensBlackList, chainId]) - - return useMemo(()=> ({ - tokens: mergedTokens, - isLoading: loading || Boolean(allTokens && !mergedTokens.length) - }), [mergedTokens, allTokens, loading]) - -} \ No newline at end of file + return [...tokens].map(([, token]) => ({ ...token })); + }, [allTokens, importedTokens, tokensBlackList, chainId, showNativeToken]); + + return useMemo( + () => ({ + tokens: mergedTokens, + isLoading: loading || Boolean(allTokens && !mergedTokens.length), + }), + [mergedTokens, allTokens, loading] + ); +}