import { Add, ArrowDropDown } from '@mui/icons-material' import clsx from 'clsx' import { useMemo, useState } from 'react' import { useForm } from 'react-hook-form' import { useTranslation } from 'react-i18next' import { HugeDecimal } from '@dao-dao/math' import { DelegationForm, StatelessDaoVoteDelegationCardProps, } from '@dao-dao/types' import { validatePercent, validatePositive, validateRequired, } from '@dao-dao/utils' import { Button } from '../buttons' import { EntityDisplay } from '../EntityDisplay' import { ErrorPage } from '../error' import { InputErrorMessage, InputLabel, NumericInput } from '../inputs' import { LineLoaders } from '../LineLoader' import { Loader } from '../logo' import { Modal } from '../modals' import { FilterableItemPopup } from '../popup' const DECIMAL_PLACES = 8 const DECIMAL_PLACES_REGEX = new RegExp(`^[0-9]+\\.[0-9]{${DECIMAL_PLACES}}$`) export const DaoVoteDelegationCard = ({ className, totalVotingPower, walletVotingPower, delegates, delegations, registration, loadingRegistration, updateRegistration, loadingDelegate, delegate, loadingUndelegate, undelegate, Trans, }: StatelessDaoVoteDelegationCardProps) => { const { t } = useTranslation() const [delegationModalOpen, setDelegationModalOpen] = useState(false) const delegationForm = useForm({ mode: 'onChange', }) const delegateEntered = delegationForm.watch('delegate') const percent = delegationForm.watch('percent') const selectedDelegateAddress = delegationForm.watch('delegate') const selectedDelegate = !delegates.loading && !delegates.errored ? delegates.data.find( ({ delegate }) => delegate === selectedDelegateAddress ) : undefined const selectedDelegateExistingDelegation = !delegations.loading && !delegations.errored ? delegations.data.find( ({ delegate }) => delegate === selectedDelegateAddress ) : undefined const addNewDelegation = () => { delegationForm.setValue('delegate', '') delegationForm.setValue('percent', '10') setDelegationModalOpen(true) } const { votingPowerToDelegate, smallestPercentDelegation, smallestPercentDelegationDoesNotTerminate, } = useMemo(() => { // Delegations module rounds down, so voting power unit determines minimum // size of delegation. const votingPowerToDelegate = !walletVotingPower.loading && !walletVotingPower.errored ? walletVotingPower.data.times(percent).div(100).trunc() : HugeDecimal.from(NaN) // Smallest percent delegation is determined by the voting power unit. const smallestPercentDelegation = !walletVotingPower.loading && !walletVotingPower.errored ? HugeDecimal.from(100).div(walletVotingPower.data).toFormattedString({ showFullAmount: true, maxNonZeroDecimals: DECIMAL_PLACES, }) : undefined const smallestPercentDelegationDoesNotTerminate = !!smallestPercentDelegation?.match(DECIMAL_PLACES_REGEX) return { votingPowerToDelegate, smallestPercentDelegation, smallestPercentDelegationDoesNotTerminate, } }, [walletVotingPower, percent]) return ( <>

{t('title.delegations')}

{registration.loading ? ( ) : registration.errored ? ( ) : registration.data.registered ? ( <>

You have been delegated{' '} {{ power: totalVotingPower.loading ? '...' : totalVotingPower.errored ? HugeDecimal.from( registration.data.power ).toFormattedString() : HugeDecimal.from(registration.data.power) .div(totalVotingPower.data) .times(100) .toFormattedString({ maxNonZeroDecimals: 3, }) + '%', }} {' '} of the total voting power.

{t('info.delegateCannotDelegate')}

) : ( <>

{t('info.voteDelegationDelegatorExplanation')}

{delegations.loading ? ( ) : delegations.errored ? ( ) : delegations.data.length > 0 ? ( <>
{delegations.data.map( ({ active, delegate, entity, percent }) => { // Whether or not the delegation is too small and rounds // down to zero based on the current voting power. const isTooSmall = !walletVotingPower.loading && !walletVotingPower.errored && walletVotingPower.data.times(percent).trunc().isZero() return ( <> {isTooSmall && smallestPercentDelegation && ( )} ) } )}
) : ( )}

{t('info.becomeDelegateExplanation', { context: delegations.loading || delegations.errored || delegations.data.length > 0 ? 'undelegate' : undefined, })}

)}
setDelegationModalOpen(false)} visible={delegationModalOpen} >
delegate(data).then( (success) => success && setDelegationModalOpen(false) ) )} >
{!delegates.loading && !delegates.errored && ( ({ key: delegate, iconUrl: entity.imageUrl, label: entity.name || entity.address, rightNode: (

{totalVotingPower.loading || totalVotingPower.errored ? HugeDecimal.from(power).toFormattedString() : HugeDecimal.from(power) .div(totalVotingPower.data) .times(100) .toFormattedString({ maxNonZeroDecimals: 3, }) + '%'}

), }))} noItemsLabel={t('info.noDelegatesFound')} onSelect={({ key }) => delegationForm.setValue('delegate', key)} trigger={{ type: 'button', props: { contentContainerClassName: 'justify-between !gap-4', size: 'lg', variant: 'ghost_outline', children: ( <> {selectedDelegate ? ( ) : (

{t('button.selectDelegate')}

)} ), }, }} /> )}
{!walletVotingPower.loading && !walletVotingPower.errored && (

{t('info.delegatesYourVotingPowerMath', { delegated: votingPowerToDelegate.isNaN() ? '—' : votingPowerToDelegate.toFormattedString({ showFullAmount: true, }), votingPower: walletVotingPower.data.toFormattedString({ showFullAmount: true, }), })} {smallestPercentDelegation && votingPowerToDelegate.isZero() && ' ' + t('info.mustDelegateMinimumDueToRounding', { context: smallestPercentDelegationDoesNotTerminate ? 'noTerminate' : 'terminates', minimum: smallestPercentDelegation, })}

)}
{selectedDelegateExistingDelegation && ( )}
) } const DELEGATE_FILTERABLE_ITEM_KEYS = ['key', 'name']