import { CopyAll } from '@mui/icons-material' import { ComponentType, useMemo, useState } from 'react' import toast from 'react-hot-toast' import { useTranslation } from 'react-i18next' import { AccountType, ButtonLinkProps, ChainId, DaoFiatDepositModalProps, IconButtonLinkProps, LoadingData, LoadingDataWithError, LoadingNfts, LoadingTokens, TokenCardInfo, TreasuryHistoryGraphProps, ValenceAccount, } from '@dao-dao/types' import { NEUTRON_GOVERNANCE_DAO, chainIsIndexed, getDisplayNameForChainId, serializeTokenSource, } from '@dao-dao/utils' import { useDao, useSupportedChainContext } from '../../../contexts' import { useButtonPopupSorter, useTokenSortOptions } from '../../../hooks' import { ErrorPage } from '../../error' import { AccountSelector } from '../../inputs' import { LineLoaders } from '../../LineLoader' import { NftSection } from '../../nft/NftSection' import { ButtonPopup } from '../../popup' import { StatusCard } from '../../StatusCard' import { TokenLineHeader } from '../../token' import { Tooltip, TooltipInfoIcon } from '../../tooltip' import { ValenceAccountsSection } from '../../ValenceAccountsSection' export type TreasuryTabProps = { connected: boolean tokens: LoadingTokens nfts: LoadingData> /** * Create cross-chain account proposal prefill URL. If undefined, this means * the action cannot be used, most likely because all accounts are already * created. */ createCrossChainAccountHref: string | undefined /** * Configure rebalancer proposal prefill URL. If undefined, this means the * action cannot be used. */ configureRebalancerHref: string | undefined FiatDepositModal: ComponentType TreasuryHistoryGraph: ComponentType TokenLine: ComponentType NftCard: ComponentType ButtonLink: ComponentType IconButtonLink: ComponentType } export const TreasuryTab = ({ connected, tokens, nfts, createCrossChainAccountHref, configureRebalancerHref, FiatDepositModal, TreasuryHistoryGraph, TokenLine, NftCard, ButtonLink, IconButtonLink, }: TreasuryTabProps) => { const { t } = useTranslation() const { chain: { chainId: currentChainId }, } = useSupportedChainContext() const { chainId: daoChainId, coreAddress, accounts } = useDao() // Combine chain tokens into loadable, lazily. Load all that are ready. const { nonValenceTokens, valenceTokens } = useMemo((): { nonValenceTokens: LoadingDataWithError valenceTokens: LoadingDataWithError } => { const chainTokens = Object.values(tokens) const allTokens: LoadingDataWithError = chainTokens.every( (l) => l?.loading ) ? { loading: true, errored: false, } : chainTokens.every((l) => l?.errored) ? { loading: false, errored: true, // First error. error: // Type-check, will always be defined. (chainTokens[0]?.errored && chainTokens[0].error) || new Error(), } : { loading: false, errored: false, updating: chainTokens.some( (l) => l && !l.loading && !l.errored && l.updating ), data: chainTokens.flatMap((l) => l && !l.loading && !l.errored ? l.data : [] ), } return { nonValenceTokens: allTokens.loading || allTokens.errored ? allTokens : { // Filter out any valence account tokens. ...allTokens, data: allTokens.data.filter( ({ owner }) => owner.type !== AccountType.Valence ), }, valenceTokens: allTokens.loading || allTokens.errored ? allTokens : { // Keep only valence account tokens. ...allTokens, data: allTokens.data.filter( ({ owner }) => owner.type === AccountType.Valence ), }, } }, [tokens]) // Combine chain tokens into loadable, lazily. Load all that are ready. const allNfts = useMemo((): LoadingDataWithError => { if (nfts.loading) { return { loading: true, errored: false, } } const chainNfts = Object.values(nfts.data) return chainNfts.every((l) => l?.loading) ? { loading: true, errored: false, } : chainNfts.every((l) => l?.errored) ? { loading: false, errored: true, // First error. error: // Type-check, will always be defined. (chainNfts[0]?.errored && chainNfts[0].error) || new Error(), } : { loading: false, errored: false, updating: chainNfts.some( (l) => l && (l.loading || (!l.errored && l.updating)) ), data: chainNfts.flatMap((l) => l && !l.loading && !l.errored ? l.data : [] ), } }, [nfts]) const [depositFiatChainId, setDepositFiatChainId] = useState< string | undefined >() // Maps serialized token source to color. const [tokenSourceColorMap, setTokenSourceColorMap] = useState< Record >({}) const tokenSortOptions = useTokenSortOptions() const { sortedData: sortedTokens, buttonPopupProps: sortTokenButtonPopupProps, } = useButtonPopupSorter({ data: nonValenceTokens.loading || nonValenceTokens.errored ? undefined : nonValenceTokens.data, options: tokenSortOptions, }) const valenceAccounts = accounts.filter( (a): a is ValenceAccount => a.type === AccountType.Valence ) return ( <> {/* header min-height of 3.5rem standardized across all tabs */}

{t('title.treasury')}

{ navigator.clipboard.writeText(address) toast.success( t('info.copiedChainAddress', { chain: getDisplayNameForChainId(chainId), }) ) }} trigger={{ type: 'button', props: { className: 'self-start', variant: 'primary', contentContainerClassName: '!gap-1', children: ( <> {t('button.copyAddress')} ), }, }} />
{ // Don't show the treasury history graph if the DAO's chain doesn't // support indexing or for the Neutron DAO for performance reasons. chainIsIndexed(daoChainId) && !( daoChainId === ChainId.NeutronMainnet && coreAddress === NEUTRON_GOVERNANCE_DAO ) && (

{t('title.treasuryValue')}

} registerTokenColors={setTokenSourceColorMap} /> ) }

{t('title.tokens')}

{nonValenceTokens.loading ? (
) : nonValenceTokens.errored ? ( ) : (
{sortedTokens.map((props: T, index) => ( ))}
)}
{t('button.createCrossChainAccount')}
{/* Show chain token load errors. */} {Object.entries(tokens).flatMap(([chainId, l]) => l && l.errored ? (
              {l.error instanceof Error ? l.error.message : l.error}
            
) : ( [] ) )} {valenceAccounts.length > 0 && ( )} {connected && !!depositFiatChainId && ( setDepositFiatChainId(undefined)} visible /> )} ) }