import { Add, Check, CopyAll, ExpandCircleDownOutlined, } from '@mui/icons-material' import clsx from 'clsx' import { useEffect, useState } from 'react' import toast from 'react-hot-toast' import { useTranslation } from 'react-i18next' import { HugeDecimal } from '@dao-dao/math' import { ButtonPopupSection, TokenCardProps, TokenType } from '@dao-dao/types' import { getFallbackImage, isNativeIbcUsdc, secondsToWdhms, shortenTokenSymbol, toAccessibleImageUrl, } from '@dao-dao/utils' import { useDaoIfAvailable } from '../../contexts' import { useAddToken } from '../../hooks' import { Button } from '../buttons/Button' import { CopyToClipboard } from '../CopyToClipboard' import { CrownIcon } from '../icons/CrownIcon' import { ButtonPopup } from '../popup' import { TooltipInfoIcon } from '../tooltip' import { Tooltip } from '../tooltip/Tooltip' import { TokenAmountDisplay } from './TokenAmountDisplay' import { UnstakingModal } from './UnstakingModal' export const TokenCard = ({ token, color, isGovernanceToken, subtitle, unstakedBalance, hasStakingInfo: _hasStakingInfo, lazyInfo, refreshUnstakingTasks, onClaim, ButtonLink, actions, EntityDisplay, }: TokenCardProps) => { const { t } = useTranslation() // If in a DAO context, don't show the DAOs governed section if the only DAO // this token governs is the current DAO. See the comment where this is used // for more details. const { coreAddress } = useDaoIfAvailable() ?? {} const lazyStakes = lazyInfo.loading || !lazyInfo.data.stakingInfo ? [] : lazyInfo.data.stakingInfo.stakes const totalStaked = lazyInfo.loading || !lazyInfo.data.stakingInfo ? HugeDecimal.zero : lazyInfo.data.stakingInfo.totalStaked const totalPendingRewards = lazyInfo.loading || !lazyInfo.data.stakingInfo ? HugeDecimal.zero : lazyInfo.data.stakingInfo.totalPendingRewards const totalUnstaking = lazyInfo.loading || !lazyInfo.data.stakingInfo ? HugeDecimal.zero : lazyInfo.data.stakingInfo.totalUnstaking const [showUnstakingTokens, setShowUnstakingTokens] = useState(false) const [copied, setCopied] = useState(false) // Debounce clearing copied. useEffect(() => { const timeout = setTimeout(() => setCopied(false), 2000) return () => clearTimeout(timeout) }, [copied]) const { isShortened, tokenSymbol, originalTokenSymbol } = shortenTokenSymbol( token.symbol ) const addToken = useAddToken() const addCw20Token = addToken && token.type === TokenType.Cw20 ? () => addToken(token.denomOrAddress) : undefined // Setup actions for popup. Prefill with cw20 related actions. const buttonPopupSections: ButtonPopupSection[] = [ { label: t('title.token'), buttons: [ { Icon: copied ? Check : CopyAll, label: token.type === TokenType.Cw20 ? t('button.copyAddressToClipboard') : t('button.copyIdToClipboard'), closeOnClick: false, onClick: () => { navigator.clipboard.writeText(token.denomOrAddress) toast.success(t('info.copiedToClipboard')) setCopied(true) }, }, ...(addCw20Token ? [ { Icon: Add, label: t('button.addToKeplr'), closeOnClick: false, onClick: addCw20Token, }, ] : []), ...(actions?.token ?? []), ], }, ...(actions?.extraSections ?? []), ] // This has staking info if we have already determined it has staking info, or // if there are any stakes or unstaking tasks once the data is loaded. For // efficiency, we don't load unstaking tasks right away because it depends on // several queries, but we can quickly check if there is anything staked and // preset `hasStakingInfo` if so. This makes sure that unstaking tasks show // even when there is nothing staked. const hasStakingInfo = _hasStakingInfo || (!lazyInfo.loading && (!!lazyInfo.data.stakingInfo?.stakes.length || !!lazyInfo.data.stakingInfo?.unstakingTasks.length)) const waitingForStakingInfo = hasStakingInfo && lazyInfo.loading return ( <>
{/* Image */}
{/* Crown */} {isGovernanceToken && ( )}
{/* Titles */}
{/* We're dealing with a token that is too long (IBC or factory probably). Instead of showing a long hash, allow the user to copy it. */} {isShortened ? ( ) : (

${tokenSymbol}

)} {color && (
)}
{!!subtitle &&

{subtitle}

}
{(waitingForStakingInfo || buttonPopupSections.length > 0) && (
)}
{/* Don't show if loading, because `unstakedBalance` will show below while loading instead. It will hide if the total loads and is the same. This prevents weird looking relayouts while also showing some balance while the total is loading. */} {!lazyInfo.loading && (

{t('info.totalHoldings')}

{/* leading-5 to match link-text's line-height. */}
{/* leading-5 to match link-text's line-height. */} {!isNativeIbcUsdc(token.chainId, token.denomOrAddress) && lazyInfo.data.usdUnitPrice?.usdPrice && // Don't calculate price if could not load token decimals // correctly. token.decimals > 0 && (
)}
)} {/* Only display `unstakedBalance` if total is loading or if different from total. While loading, the total above will hide. */} {(lazyInfo.loading || !lazyInfo.data.totalBalance.eq(unstakedBalance)) && (

{t('info.availableBalance')}

{/* leading-5 to match link-text's line-height. */} {!isNativeIbcUsdc(token.chainId, token.denomOrAddress) && (lazyInfo.loading || lazyInfo.data.usdUnitPrice?.usdPrice) && // Don't calculate price if could not load token decimals // correctly. token.decimals > 0 && (
)}
)}
{hasStakingInfo && (

{t('info.stakes')}

{t('title.staked')}

{t('title.stakedTo')}

{lazyInfo.loading ? '...' : lazyStakes.length > 0 && ( <> {lazyStakes[0].validator.moniker} {lazyStakes.length > 1 && ( <> ,{' '} {lazyStakes .slice(1) .map(({ validator }, index) => (

{validator.moniker}

))} } > {t('info.andNumMore', { count: lazyStakes.length - 1, })} )} )}

{t('title.unstaking')}

{t('info.pendingRewards')}

)} {!lazyInfo.loading && !!lazyInfo.data.daosGoverned?.length && // Only show DAOs if there are more than 1 or if the only DAO in the // list is the current DAO. This prevents the DAO's governance token // from listing only the DAO we're currently viewing as a DAO it // governs, since that would be unhelpful. When there are multiple // DAOs, we show them all, because it would be confusing to not show // the current DAO and it helps provide context. (!coreAddress || lazyInfo.data.daosGoverned.length > 1 || lazyInfo.data.daosGoverned[0].coreAddress !== coreAddress) && (

{t('title.daosGoverned')}

{lazyInfo.data.daosGoverned.map( ({ coreAddress, stakedBalance }) => (
{/* Only show staked balance if defined and nonzero. */} {!!stakedBalance && ( )}
) )}
)}
{!lazyInfo.loading && lazyInfo.data.stakingInfo && ( setShowUnstakingTokens(false)} refresh={refreshUnstakingTasks} tasks={lazyInfo.data.stakingInfo.unstakingTasks} unstakingDuration={ lazyInfo.data.stakingInfo.unstakingDurationSeconds ? secondsToWdhms( lazyInfo.data.stakingInfo.unstakingDurationSeconds ) : undefined } visible={showUnstakingTokens} /> )} ) }