diff --git a/src/components/CoinBalanceUSD.tsx b/src/components/CoinBalanceUSD.tsx index 09a2843..307760c 100644 --- a/src/components/CoinBalanceUSD.tsx +++ b/src/components/CoinBalanceUSD.tsx @@ -16,10 +16,10 @@ export const CoinBalanceUSD = (props: ComponentProps) => { const { tokenPriceMap, getUSDValue } = useUSDValue(); const address = tokenInfo.address; const cgPrice = address ? tokenPriceMap[address]?.usd || 0 : 0; - const amountInUSD = useMemo(() => { - if (!amount || !hasNumericValue(amount)) return 0; - return new Decimal(amount).mul(cgPrice).toNumber(); + const amountInUSD = useMemo(() => { + if (!amount || !hasNumericValue(amount)) return new Decimal(0); + return new Decimal(amount).mul(cgPrice) }, [amount, cgPrice]); // effects @@ -27,8 +27,6 @@ export const CoinBalanceUSD = (props: ComponentProps) => { if (address) getUSDValue([address]); }, [address, getUSDValue]); - if (!amountInUSD || amountInUSD <= 0) return <>{''}; - return ( <> {prefix}${formatNumber.format(amountInUSD, maxDecimals || 2)} diff --git a/src/components/FormPairRow.tsx b/src/components/FormPairRow.tsx index abc24ef..8435b02 100644 --- a/src/components/FormPairRow.tsx +++ b/src/components/FormPairRow.tsx @@ -1,73 +1,182 @@ +import React, { + CSSProperties, + useEffect, + useMemo, + useRef, +} from 'react'; import { TokenInfo } from '@solana/spl-token-registry'; -import React, { CSSProperties, useMemo } from 'react'; - -import CoinBalance from './Coinbalance'; -import { PAIR_ROW_HEIGHT } from './FormPairSelector'; -import TokenIcon from './TokenIcon'; -import TokenLink from './TokenLink'; -import { useUSDValueProvider } from 'src/contexts/USDValueProvider'; import Decimal from 'decimal.js'; +import { WRAPPED_SOL_MINT } from 'src/constants'; +import { checkIsStrictOrVerified, checkIsToken2022, checkIsUnknownToken } from 'src/misc/tokenTags'; import { useAccounts } from 'src/contexts/accounts'; +import { formatNumber } from 'src/misc/utils'; +import TokenIcon from './TokenIcon'; +import TokenLink from './TokenLink'; +import CoinBalance from './Coinbalance'; + +export const PAIR_ROW_HEIGHT = 72; -const FormPairRow: React.FC<{ +export interface IPairRow { + usdValue?: Decimal; item: TokenInfo; style: CSSProperties; onSubmit(item: TokenInfo): void; -}> = ({ item, style, onSubmit }) => { - const isUnknown = useMemo(() => item.tags?.length === 0 || item.tags?.includes('unknown'), [item.tags]) + suppressCloseModal?: boolean; + showExplorer?: boolean; + enableUnknownTokenWarning?: boolean; + isLST?: boolean; +} +interface IMultiTag { + isVerified: boolean; + isLST: boolean; + isUnknown: boolean; + isToken2022: boolean; + isFrozen: boolean; +} + +const MultiTags: React.FC = ({ item }) => { const { accounts } = useAccounts(); - const { tokenPriceMap } = useUSDValueProvider(); + const isLoading = useRef(false); + const isLoaded = useRef(false); + // It's cheaper to slightly delay and rendering once, than rendering everything all the time + const [renderedTag, setRenderedTag] = React.useState({ + isVerified: false, + isLST: false, + isUnknown: false, + isToken2022: false, + isFrozen: false, + }); + + useEffect(() => { + if (isLoaded.current || isLoading.current) return; + + isLoading.current = true; + setTimeout(() => { + const result = { + isVerified: checkIsStrictOrVerified(item), + isLST: Boolean(item.tags?.includes('lst')), + isUnknown: checkIsUnknownToken(item), + isToken2022: Boolean(checkIsToken2022(item)), + isFrozen: accounts[item.address]?.isFrozen || false, + }; + setRenderedTag(result); + isLoading.current = false; + isLoaded.current = true; + }, 0); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const remainingTags = useMemo(() => { + // Filter out the tags we've already used + const filterTags = ['verified', 'strict', 'lst', 'unknown', 'token-2022', 'new']; + const otherTags = item.tags?.filter((item) => filterTags.includes(item) === false); + return otherTags; + }, [item.tags]); + + if (!renderedTag) return null; + + const { isVerified, isLST, isUnknown, isToken2022, isFrozen } = renderedTag; + + return ( +
+ {isFrozen && ( +

+ Frozen +

+ )} - const totalUsdValue = useMemo(() => { - const tokenPrice = tokenPriceMap[item.address]?.usd; - const balance = accounts[item.address]?.balance; - if (!tokenPrice || !balance) return null; + {isUnknown && ( +

+ Unknown +

+ )} - const totalAValue = new Decimal(tokenPrice).mul(balance); - return totalAValue; - }, [accounts, item.address, tokenPriceMap]) + {isToken2022 && ( +

+ Token2022 +

+ )} + + {remainingTags?.map((tag, idx) => ( +
+ {tag} +
+ ))} + + {isLST && ( +

+ LST +

+ )} + + {isVerified && ( +

+ {/* We're renaming verified to stict for now, requested by Mei */} + Strict +

+ )} +
+ ); +}; + +const FormPairRow = (props: IPairRow) => { + const { + item, + style, + onSubmit, + suppressCloseModal, + usdValue, + showExplorer = true, + enableUnknownTokenWarning = true, + } = props; + const onClick = React.useCallback(() => { + onSubmit(item); + + if (suppressCloseModal) return; + }, [onSubmit, item, suppressCloseModal]); + + const usdValueDisplay = + usdValue && usdValue.gt(0.01) // If smaller than 0.01 cents, dont show + ? `$${formatNumber.format(usdValue.toDP(2).toNumber())}` + : ''; return (
  • -
    onSubmit(item)} - > +
    -
    - +
    +
    -
    -
    -

    - {item.symbol} -

    - -
    - -
    - - - {totalUsdValue && totalUsdValue.gt(0.01) ? ( - - | ${totalUsdValue.toFixed(2)} - +
    +

    {item.symbol}

    + {/* Intentionally higher z to be clickable */} + {showExplorer ? ( +
    e.stopPropagation()}> + +
    ) : null}
    +

    + {item.address === WRAPPED_SOL_MINT.toBase58() ? 'Solana' : item.name} +

    - {isUnknown ? ( -

    - Unknown -

    - ) : null} +
    + + {usdValueDisplay ?

    {usdValueDisplay}

    : null} + +
  • ); diff --git a/src/components/FormPairSelector.tsx b/src/components/FormPairSelector.tsx index 6986c58..bc05509 100644 --- a/src/components/FormPairSelector.tsx +++ b/src/components/FormPairSelector.tsx @@ -26,7 +26,7 @@ const rowRenderer = memo((props: ListChildComponentProps) => { const { data, index, style } = props; const item = data.searchResult[index]; - return ; + return ; }, areEqual); const generateSearchTerm = (info: TokenInfo, searchValue: string) => { @@ -126,7 +126,6 @@ const FormPairSelector = ({ onSubmit, tokenInfos, onClose }: IFormPairSelector) const userWalletResults = useMemo(() => { const userWalletResults: TokenInfo[] = [...tokenMap.values(), ...unknownTokenMap.values()]; - getUSDValue(userWalletResults.map((item) => item.address)); return userWalletResults; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -144,7 +143,6 @@ const FormPairSelector = ({ onSubmit, tokenInfos, onClose }: IFormPairSelector) // Show user wallet tokens by default if (!searchValue.current) { setSearchResult(await sortTokenListByBalance(userWalletResults)); - console.log({userWalletResults}) setIsSearching(false); return; } @@ -285,6 +283,7 @@ const FormPairSelector = ({ onSubmit, tokenInfos, onClose }: IFormPairSelector) itemData={{ searchResult, onSubmit, + mintToUsdValue, }} className={classNames('overflow-y-scroll mr-1 min-h-[12rem] px-5 webkit-scrollbar')} > diff --git a/src/components/PriceInfo/Fees.tsx b/src/components/PriceInfo/Fees.tsx index 694a9d8..31fdac9 100644 --- a/src/components/PriceInfo/Fees.tsx +++ b/src/components/PriceInfo/Fees.tsx @@ -24,7 +24,7 @@ const Fees = ({ routePlan }: IFees) => { const decimals = tokenMint?.decimals ?? 6; const feeAmount = formatNumber.format( - new Decimal(item.swapInfo.feeAmount.toString()).div(Math.pow(10, decimals)).toNumber(), + new Decimal(item.swapInfo.feeAmount.toString()).div(Math.pow(10, decimals)), ); const feePct = new Decimal(item.swapInfo.feeAmount.toString()) .div( @@ -45,7 +45,7 @@ const Fees = ({ routePlan }: IFees) => {
    - {feeAmount} {tokenMint?.symbol} ({formatNumber.format(new Decimal(feePct).mul(100).toNumber())} + {feeAmount} {tokenMint?.symbol} ({formatNumber.format(new Decimal(feePct).mul(100))} %)
    diff --git a/src/components/SwapSettingsModal/SwapSettingsModal.tsx b/src/components/SwapSettingsModal/SwapSettingsModal.tsx index 6f2dac5..413c2a0 100644 --- a/src/components/SwapSettingsModal/SwapSettingsModal.tsx +++ b/src/components/SwapSettingsModal/SwapSettingsModal.tsx @@ -15,7 +15,7 @@ import { useSwapContext } from 'src/contexts/SwapContext'; import { useWalletPassThrough } from 'src/contexts/WalletPassthroughProvider'; import ExternalIcon from 'src/icons/ExternalIcon'; import { SOL_TOKEN_INFO } from 'src/misc/constants'; -import { detectedSeparator, formatNumber, toLamports } from 'src/misc/utils'; +import { detectedSeparator, formatNumber, hasNumericValue, toLamports } from 'src/misc/utils'; import { useReferenceFeesQuery } from 'src/queries/useReferenceFeesQuery'; import { CoinBalanceUSD } from '../CoinBalanceUSD'; import JupButton from '../JupButton'; @@ -353,7 +353,7 @@ const SwapSettingsModal: React.FC<{ closeModal: () => void }> = ({ closeModal }) @@ -370,7 +370,7 @@ const SwapSettingsModal: React.FC<{ closeModal: () => void }> = ({ closeModal }) { - if (typeof floatValue !== 'number') return; + if (typeof floatValue !== 'number' || floatValue <= 0) return; form.setValue('hasUnsavedFeeChanges', true); onChange(floatValue); }} diff --git a/src/components/useSortByValue.tsx b/src/components/useSortByValue.tsx index dbc2409..8d3a3d9 100644 --- a/src/components/useSortByValue.tsx +++ b/src/components/useSortByValue.tsx @@ -1,12 +1,12 @@ -import { TokenInfo } from "@solana/spl-token-registry"; -import Decimal from "decimal.js"; -import { useCallback, useEffect, useRef } from "react"; -import { WRAPPED_SOL_MINT } from "src/constants"; -import { useTokenContext } from "src/contexts/TokenContextProvider"; -import { useUSDValue } from "src/contexts/USDValueProvider"; -import { useAccounts } from "src/contexts/accounts"; -import { checkIsUnknownToken } from "src/misc/tokenTags"; -import { fromLamports } from "src/misc/utils"; +import { TokenInfo } from '@solana/spl-token-registry'; +import Decimal from 'decimal.js'; +import { useCallback, useEffect, useRef } from 'react'; +import { WRAPPED_SOL_MINT } from 'src/constants'; +import { useTokenContext } from 'src/contexts/TokenContextProvider'; +import { useUSDValue } from 'src/contexts/USDValueProvider'; +import { useAccounts } from 'src/contexts/accounts'; +import { checkIsUnknownToken } from 'src/misc/tokenTags'; +import { fromLamports } from 'src/misc/utils'; export const useSortByValue = () => { const { getTokenInfo } = useTokenContext(); diff --git a/src/contexts/SwapContext.tsx b/src/contexts/SwapContext.tsx index 5dc11ac..b85c1a5 100644 --- a/src/contexts/SwapContext.tsx +++ b/src/contexts/SwapContext.tsx @@ -273,15 +273,21 @@ export const SwapContextProvider: FC<{ setForm((prev) => { const newValue = { ...prev }; + if (!fromTokenInfo || !toTokenInfo) return prev; + let { inAmount, outAmount } = quoteResponseMeta?.quoteResponse || {}; if (jupiterSwapMode === SwapMode.ExactIn) { - newValue.toValue = outAmount ? String(fromLamports(outAmount, toTokenInfo?.decimals || 0)) : ''; + newValue.toValue = outAmount + ? new Decimal(outAmount.toString()).div(10 ** toTokenInfo.decimals).toFixed() + : ''; } else { - newValue.fromValue = inAmount ? String(fromLamports(inAmount, fromTokenInfo?.decimals || 0)) : ''; + newValue.fromValue = inAmount + ? new Decimal(inAmount.toString()).div(10 ** fromTokenInfo.decimals).toFixed() + : ''; } return newValue; }); - }, [form.fromValue, fromTokenInfo?.decimals, jupiterSwapMode, quoteResponseMeta, toTokenInfo?.decimals]); + }, [form.fromValue, fromTokenInfo, jupiterSwapMode, quoteResponseMeta, toTokenInfo]); const [txStatus, setTxStatus] = useState< | { diff --git a/src/contexts/USDValueProvider.tsx b/src/contexts/USDValueProvider.tsx index 80a228c..a0b7220 100644 --- a/src/contexts/USDValueProvider.tsx +++ b/src/contexts/USDValueProvider.tsx @@ -166,24 +166,6 @@ export const USDValueProvider: FC<{ children: ReactNode }> = ({ children }) => { ); }, [setCachedPrices]); - useEffect(() => { - if (!Object.keys(accounts).length) return; - - const userAccountAddresses: string[] = Object.keys(accounts) - .map((key) => { - const token = getTokenInfo(key); - - if (!token) return undefined; - - return token.address; - }) - .filter(Boolean) as string[]; - - setAddresses((prev) => { - return new Set([...prev, ...userAccountAddresses]); - }); - }, [accounts, getTokenInfo]); - // Make sure form token always have USD values useEffect(() => { setAddresses((prev) => { @@ -209,6 +191,27 @@ export const USDValueProvider: FC<{ children: ReactNode }> = ({ children }) => { }); }, []); + useEffect(() => { + if (!Object.keys(accounts).length) return; + + const userAccountAddresses: string[] = Object.keys(accounts) + .map((key) => { + const token = getTokenInfo(key); + + if (!token) return undefined; + + return token.address; + }) + .filter(Boolean) as string[]; + + // Fetch USD value + getUSDValue(userAccountAddresses); + + setAddresses((prev) => { + return new Set([...prev, ...userAccountAddresses]); + }); + }, [accounts, getTokenInfo, getUSDValue]); + return ( {children} diff --git a/src/contexts/accounts.tsx b/src/contexts/accounts.tsx index a4aa846..8d11737 100644 --- a/src/contexts/accounts.tsx +++ b/src/contexts/accounts.tsx @@ -16,6 +16,7 @@ export interface IAccountsBalance { balanceLamports: BN; hasBalance: boolean; decimals: number; + isFrozen: boolean; } interface IAccountContext { @@ -33,7 +34,7 @@ interface ParsedTokenData { isNative: boolean; mint: string; owner: string; - state: string; + state: number; tokenAmount: { amount: string; decimals: number; @@ -153,6 +154,7 @@ const AccountsProvider: React.FC = ({ children }) => { balanceLamports: new BN(response?.lamports || 0), hasBalance: response?.lamports ? response?.lamports > 0 : false, decimals: 9, + isFrozen: false, }; } }, [publicKey, connected, connection]); @@ -174,10 +176,11 @@ const AccountsProvider: React.FC = ({ children }) => { acc[item.account.data.parsed.info.mint] = { balance: item.account.data.parsed.info.tokenAmount.uiAmount, - balanceLamports: new BN(0), + balanceLamports: new BN(item.account.data.parsed.info.tokenAmount.amount), pubkey: item.pubkey, hasBalance: item.account.data.parsed.info.tokenAmount.uiAmount > 0, decimals: item.account.data.parsed.info.tokenAmount.decimals, + isFrozen: item.account.data.parsed.info.state === 2, // 2 is frozen }; return acc; }, diff --git a/src/misc/utils.ts b/src/misc/utils.ts index 20fd7f1..e4e0e05 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -19,16 +19,23 @@ export const numberFormatter = new Intl.NumberFormat(userLocale, { }); export const formatNumber = { - format: (val?: number, precision?: number) => { + format: (val?: number | Decimal, precision?: number) => { if (!val && val !== 0) { return '--'; } - if (precision !== undefined) { - return val.toFixed(precision); - } else { - return numberFormatter.format(val); + // Force numberFormatter to respect the desired precision + const numberFormatter = new Intl.NumberFormat(userLocale, { + style: 'decimal', + minimumFractionDigits: precision, + maximumFractionDigits: 12, + }); + + if (typeof val === 'number') { + return numberFormatter.format(precision !== undefined ? +val.toFixed(precision) : val); } + + return numberFormatter.format(precision !== undefined ? Number(val.toFixed(precision)) : val.toNumber()); }, };