diff --git a/modules/delegation/constants.ts b/modules/delegation/constants.ts index c73c9134..f0316939 100644 --- a/modules/delegation/constants.ts +++ b/modules/delegation/constants.ts @@ -1,9 +1,6 @@ -import { BigNumber } from 'ethers' - export const SNAPSHOT_LIDO_SPACE_NAME = '0x6c69646f2d736e617073686f742e657468000000000000000000000000000000' // lido-snapshot.eth export const DELEGATORS_PAGE_SIZE = 10 export const DELEGATORS_FETCH_SIZE = 50 export const DELEGATORS_FETCH_TOTAL = 200 -export const VP_MIN_TO_SHOW = BigNumber.from(10).pow(16) diff --git a/modules/delegation/hooks/useDelegators.ts b/modules/delegation/hooks/useDelegators.ts index 4e212cd8..ae30b546 100644 --- a/modules/delegation/hooks/useDelegators.ts +++ b/modules/delegation/hooks/useDelegators.ts @@ -2,41 +2,47 @@ import { CHAINS } from '@lido-sdk/constants' import { useLidoSWR } from '@lido-sdk/react' import { ContractVoting } from 'modules/blockChain/contracts' import { useWeb3 } from 'modules/blockChain/hooks/useWeb3' -import { - DELEGATORS_FETCH_SIZE, - DELEGATORS_FETCH_TOTAL, - VP_MIN_TO_SHOW, -} from '../constants' +import { DELEGATORS_FETCH_SIZE, DELEGATORS_FETCH_TOTAL } from '../constants' import { BigNumber } from 'ethers' +type DelegatorData = { address: string; balance: BigNumber } + +type DelegatorsData = { + nonZeroDelegators: DelegatorData[] + totalVotingPower: BigNumber + notFetchedDelegatorsCount: number +} + +/* + SWR data hook to fetch first N delegators of the current wallet address. + Returns up to DELEGATORS_FETCH_TOTAL delegators with their voting power. + The list contains only delegators with voting power greater than 0. +*/ export function useDelegators() { const { walletAddress, chainId } = useWeb3() const voting = ContractVoting.useRpc() - return useLidoSWR( - walletAddress - ? [`swr:useDelegatorsPaginatedList`, chainId, walletAddress] - : null, + + const { data, initialLoading, loading, error } = useLidoSWR( + walletAddress ? [`swr:useDelegators`, chainId, walletAddress] : null, async (_key: string, _chainId: CHAINS, _walletAddress: string) => { - const delegatorsCount = ( + const totalDelegatorsCount = ( await voting.getDelegatedVotersCount(_walletAddress) ).toNumber() - if (delegatorsCount === 0) { + if (totalDelegatorsCount === 0) { return { - totalCount: 0, - fetchedCount: 0, - wealthyCount: 0, - list: [] as { address: string; balance: BigNumber }[], - fetchedValue: 0, + nonZeroDelegators: [] as DelegatorData[], + totalVotingPower: BigNumber.from(0), + notFetchedDelegatorsCount: 0, } } - const fetchLimit = Math.min(delegatorsCount, DELEGATORS_FETCH_TOTAL) + const fetchLimit = Math.min(totalDelegatorsCount, DELEGATORS_FETCH_TOTAL) const fetchCount = Math.ceil(fetchLimit / DELEGATORS_FETCH_SIZE) const fetchNumbers = Array(fetchCount).fill(0) - const delegators: { address: string; balance: BigNumber }[] = [] - let fetchedValue = BigNumber.from(0) + const delegators: DelegatorData[] = [] + let totalVotingPower = BigNumber.from(0) await Promise.all( fetchNumbers.map(async (_, fetchIndex) => { @@ -59,23 +65,33 @@ export function useDelegators() { address: delegator, balance: delegatorsAtPageBalances[index], }) - fetchedValue = fetchedValue.add(delegatorsAtPageBalances[index]) + totalVotingPower = totalVotingPower.add( + delegatorsAtPageBalances[index], + ) }) }), ) - const fetchedCount = delegators.length - - const wealthyDelegators = delegators.filter(delegator => - delegator.balance.gt(VP_MIN_TO_SHOW), + const nonZeroDelegators = delegators.filter(delegator => + delegator.balance.gt(0), ) + return { - totalCount: delegatorsCount, - fetchedCount, - wealthyCount: wealthyDelegators.length, - list: wealthyDelegators, - fetchedValue, + nonZeroDelegators, + totalVotingPower, + notFetchedDelegatorsCount: totalDelegatorsCount - delegators.length, } }, ) + + return { + data: { + nonZeroDelegators: data?.nonZeroDelegators ?? [], + totalVotingPower: data?.totalVotingPower ?? BigNumber.from(0), + notFetchedDelegatorsCount: data?.notFetchedDelegatorsCount ?? 0, + } as DelegatorsData, + initialLoading, + loading, + error, + } } diff --git a/modules/delegation/hooks/useDelegatorsInfo.ts b/modules/delegation/hooks/useDelegatorsInfo.ts deleted file mode 100644 index a98bfedc..00000000 --- a/modules/delegation/hooks/useDelegatorsInfo.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useDelegators } from './useDelegators' - -export function useDelegatorsInfo() { - const { data, initialLoading, loading } = useDelegators() - - return { - data: { - totalCount: data?.totalCount ?? 0, - fetchedCount: data?.fetchedCount ?? 0, - wealthyCount: data?.wealthyCount ?? 0, - fetchedValue: data?.fetchedValue ?? 0, - }, - loading, - initialLoading, - } -} diff --git a/modules/delegation/hooks/useDelegatorsPaginatedList.ts b/modules/delegation/hooks/useDelegatorsPaginatedList.ts deleted file mode 100644 index ffb36ed1..00000000 --- a/modules/delegation/hooks/useDelegatorsPaginatedList.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { DELEGATORS_PAGE_SIZE } from '../constants' -import { useDelegators } from './useDelegators' - -export function useDelegatorsPaginatedList(pageNumber: number) { - const { data, initialLoading, loading } = useDelegators() - if (pageNumber < 0 || !data?.list) { - return { data: [], initialLoading, loading } - } - const delegators = data.list - delegators.sort((prev, next) => (prev.balance.gt(next.balance) ? -1 : 1)) - - const delegatorsPage = delegators.slice( - pageNumber * DELEGATORS_PAGE_SIZE, - (pageNumber + 1) * DELEGATORS_PAGE_SIZE, - ) - return { - data: delegatorsPage, - initialLoading, - loading, - } -} diff --git a/modules/delegation/ui/DelegatorsList/DelegatorsList.tsx b/modules/delegation/ui/DelegatorsList/DelegatorsList.tsx index dcad7a47..4039f3e7 100644 --- a/modules/delegation/ui/DelegatorsList/DelegatorsList.tsx +++ b/modules/delegation/ui/DelegatorsList/DelegatorsList.tsx @@ -8,33 +8,37 @@ import { CounterBadge, TitleWrap, } from './DelegatorsListStyle' -import { DelegatorsListPage } from './DelegatorsListPage' -import { useDelegatorsInfo } from 'modules/delegation/hooks/useDelegatorsInfo' import { PageLoader } from 'modules/shared/ui/Common/PageLoader' import { DELEGATORS_FETCH_TOTAL, DELEGATORS_PAGE_SIZE, } from 'modules/delegation/constants' -import { useGovernanceSymbol } from 'modules/tokens/hooks/useGovernanceSymbol' import { InfoLabel } from 'modules/shared/ui/Common/InfoRow' import { getEtherscanAddressLink } from '@lido-sdk/helpers' import { AragonVoting } from 'modules/blockChain/contractAddresses' import { formatBalance } from 'modules/blockChain/utils/formatBalance' +import { useDelegators } from 'modules/delegation/hooks/useDelegators' +import { ExternalLink } from 'modules/shared/ui/Common/ExternalLink' +import { useGovernanceBalance } from 'modules/tokens/hooks/useGovernanceBalance' +import { DelegatorsListItem } from './DelegatorsListItem' + +const DAO_OPS_FORUM_LINK = + 'https://research.lido.fi/new-message?groupname=DAO_Ops' export function DelegatorsList() { const { isWalletConnected, chainId } = useWeb3() const [pageCount, setPageCount] = useState(1) - const { data: governanceSymbol } = useGovernanceSymbol() + const { data: governanceToken } = useGovernanceBalance() - const { data, initialLoading } = useDelegatorsInfo() + const { data, initialLoading } = useDelegators() - const pages = useMemo(() => { - const result = [] - for (let i = 0; i < pageCount; i++) { - result.push() + const delegatorsToShow = useMemo(() => { + if (!data.nonZeroDelegators.length) { + return [] } - return result - }, [pageCount]) + + return data.nonZeroDelegators.slice(0, pageCount * DELEGATORS_PAGE_SIZE) + }, [data.nonZeroDelegators, pageCount]) if (!isWalletConnected) { return ( @@ -50,7 +54,9 @@ export function DelegatorsList() { return } - if (data.wealthyCount === 0) { + const nonZeroDelegatorsCount = data.nonZeroDelegators.length + + if (nonZeroDelegatorsCount === 0) { return ( @@ -60,34 +66,40 @@ export function DelegatorsList() { ) } - const outOfList = data.totalCount - data.fetchedCount - return ( Delegated - {formatBalance(data.fetchedValue || 0)} {governanceSymbol} + {formatBalance(data.totalVotingPower)} {governanceToken?.symbol} - from {data.wealthyCount} address - {data.wealthyCount > 1 ? 'es' : ''} on-chain + from {nonZeroDelegatorsCount} address + {nonZeroDelegatorsCount > 1 ? 'es' : ''} on-chain - {pages} - {data.wealthyCount > pageCount * DELEGATORS_PAGE_SIZE && ( + {delegatorsToShow.map(delegator => ( + + ))} + {nonZeroDelegatorsCount > pageCount * DELEGATORS_PAGE_SIZE && ( setPageCount(count => count + 1)}> Show More )} - {outOfList > 0 && ( - - The voting power list displays addresses with a positive LDO balance - from the first{` ${DELEGATORS_FETCH_TOTAL} `}delegators. You have - {` ${outOfList} `}more delegator{outOfList > 1 ? 's' : ''} who were - not included in the list. To see all your delegators, use the{' '} + {data.notFetchedDelegatorsCount > 0 && ( + + This list displays addresses with a positive {governanceToken?.symbol}{' '} + balance from the first {DELEGATORS_FETCH_TOTAL} delegators. You have{' '} + {data.notFetchedDelegatorsCount} more delegator + {data.notFetchedDelegatorsCount > 1 ? 's' : ''} who were not included + in the list. To see all your delegators, use the{' '} . If needed, contact the{' '} - - DAO Ops{' '} - {' '} - on the forum for assistance. + DAO Ops on the + forum for assistance. )} diff --git a/modules/delegation/ui/DelegatorsList/DelegatorsListItem.tsx b/modules/delegation/ui/DelegatorsList/DelegatorsListItem.tsx new file mode 100644 index 00000000..8e2ee95a --- /dev/null +++ b/modules/delegation/ui/DelegatorsList/DelegatorsListItem.tsx @@ -0,0 +1,37 @@ +import { Identicon, Text, trimAddress } from '@lidofinance/lido-ui' +import { AddressPop } from 'modules/shared/ui/Common/AddressPop' +import { + AddressBadgeWrap, + DelegatorsListItemStyled, +} from './DelegatorsListStyle' +import { formatBalance } from 'modules/blockChain/utils/formatBalance' +import { BigNumber } from 'ethers' + +type Props = { + address: string + balance: BigNumber + governanceSymbol: string | undefined +} + +export function DelegatorsListItem({ + address, + balance, + governanceSymbol, +}: Props) { + return ( + + + + + + {/* {(ensNameList && ensNameList[i]) || trimAddress(address, 4)} */} + {trimAddress(address, 4)} + + + + + {formatBalance(balance)} {governanceSymbol ?? ''} + + + ) +} diff --git a/modules/delegation/ui/DelegatorsList/DelegatorsListPage.tsx b/modules/delegation/ui/DelegatorsList/DelegatorsListPage.tsx deleted file mode 100644 index bbed8e6f..00000000 --- a/modules/delegation/ui/DelegatorsList/DelegatorsListPage.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Fragment, useMemo } from 'react' -import { Identicon, Text, trimAddress } from '@lidofinance/lido-ui' -import { useDelegatorsPaginatedList } from 'modules/delegation/hooks/useDelegatorsPaginatedList' -import { AddressPop } from 'modules/shared/ui/Common/AddressPop' -import { PageLoader } from 'modules/shared/ui/Common/PageLoader' -import { AddressBadgeWrap, DelegatorsListItem } from './DelegatorsListStyle' -import { formatBalance } from 'modules/blockChain/utils/formatBalance' -import { useGovernanceSymbol } from 'modules/tokens/hooks/useGovernanceSymbol' -import { useEnsNames } from 'modules/shared/hooks/useEnsNames' - -type Props = { - pageNumber: number -} - -export function DelegatorsListPage({ pageNumber }: Props) { - const { data, initialLoading } = useDelegatorsPaginatedList(pageNumber) - const { data: governanceSymbol } = useGovernanceSymbol() - - const addresses = useMemo(() => data.map(item => item.address), [data]) - - const { data: ensNameList } = useEnsNames(addresses) - if (initialLoading) { - return - } - - return ( - - {data.map((delegator, i) => ( - - - - - - {(ensNameList && ensNameList[i]) || - trimAddress(delegator.address, 4)} - - - - - {formatBalance(delegator.balance)} {governanceSymbol} - - - ))} - - ) -} diff --git a/modules/delegation/ui/DelegatorsList/DelegatorsListStyle.ts b/modules/delegation/ui/DelegatorsList/DelegatorsListStyle.ts index 4782a4c6..bd419488 100644 --- a/modules/delegation/ui/DelegatorsList/DelegatorsListStyle.ts +++ b/modules/delegation/ui/DelegatorsList/DelegatorsListStyle.ts @@ -10,6 +10,7 @@ export const Wrap = styled.div<{ $empty?: boolean }>` gap: 12px; max-width: 496px; margin: 0 auto; + text-align: center; ${({ $empty }) => $empty && @@ -46,7 +47,7 @@ export const AddressBadgeWrap = styled.span` } ` -export const DelegatorsListItem = styled.div` +export const DelegatorsListItemStyled = styled.div` padding: 12px 20px; display: flex; align-items: center;