diff --git a/assets/avatar.com.svg.react b/assets/avatar.com.svg.react new file mode 100644 index 00000000..85e2c590 --- /dev/null +++ b/assets/avatar.com.svg.react @@ -0,0 +1,4 @@ + + + + diff --git a/assets/lido.social.com.svg.react b/assets/lido.social.com.svg.react new file mode 100644 index 00000000..b56e2880 --- /dev/null +++ b/assets/lido.social.com.svg.react @@ -0,0 +1,3 @@ + + + diff --git a/assets/x.social.com.svg.react b/assets/x.social.com.svg.react new file mode 100644 index 00000000..8e8570ca --- /dev/null +++ b/assets/x.social.com.svg.react @@ -0,0 +1,3 @@ + + + diff --git a/modules/blockChain/utils/formatBalance.ts b/modules/blockChain/utils/formatBalance.ts index 593171b6..6a2a2e9e 100644 --- a/modules/blockChain/utils/formatBalance.ts +++ b/modules/blockChain/utils/formatBalance.ts @@ -5,6 +5,7 @@ const formatter = Intl.NumberFormat('en', { notation: 'compact', maximumSignificantDigits: 3, }) + export const formatBalance = (amount: BigNumberish) => { return formatter.format(weiToNum(amount)) } 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..0175be77 100644 --- a/modules/delegation/hooks/useDelegators.ts +++ b/modules/delegation/hooks/useDelegators.ts @@ -2,41 +2,53 @@ 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' +import { useEnsResolvers } from 'modules/shared/hooks/useEnsResolvers' +type DelegatorData = { + address: string + balance: BigNumber + ensName?: string | null +} + +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 { lookupAddress } = useEnsResolvers() + + 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 +71,48 @@ export function useDelegators() { address: delegator, balance: delegatorsAtPageBalances[index], }) - fetchedValue = fetchedValue.add(delegatorsAtPageBalances[index]) + totalVotingPower = totalVotingPower.add( + delegatorsAtPageBalances[index], + ) }) }), ) - const fetchedCount = delegators.length + const nonZeroDelegators = delegators.filter(delegator => + delegator.balance.gt(0), + ) - const wealthyDelegators = delegators.filter(delegator => - delegator.balance.gt(VP_MIN_TO_SHOW), + const nonZeroDelegatorsWithEns = await Promise.all( + nonZeroDelegators.map(async delegator => { + try { + const ensName = await lookupAddress(delegator.address) + + return { + ...delegator, + ensName, + } + } catch (err) { + return delegator + } + }), ) + return { - totalCount: delegatorsCount, - fetchedCount, - wealthyCount: wealthyDelegators.length, - list: wealthyDelegators, - fetchedValue, + nonZeroDelegators: nonZeroDelegatorsWithEns, + 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/providers/DelegateFromPublicListContext.tsx b/modules/delegation/providers/DelegateFromPublicListContext.tsx new file mode 100644 index 00000000..36ba49f2 --- /dev/null +++ b/modules/delegation/providers/DelegateFromPublicListContext.tsx @@ -0,0 +1,50 @@ +import { createContext, FC, useCallback, useContext, useState } from 'react' +import invariant from 'tiny-invariant' + +// +// Data context +// + +type Value = { + selectedPublicDelegate: string | undefined + onPublicDelegateSelect: (address: string) => () => void + onPublicDelegateReset: () => void +} + +const DelegateFromPublicListContext = createContext(null) + +export const useDelegateFromPublicList = () => { + const value = useContext(DelegateFromPublicListContext) + invariant( + value, + 'useDelegateFromPublicList was used outside the DelegateFromPublicListContext provider', + ) + return value +} + +export const DelegateFromPublicListProvider: FC = ({ children }) => { + const [selectedPublicDelegate, setSelectedPublicDelegate] = useState() + + const handleDelegatePick = useCallback( + (address: string) => () => { + setSelectedPublicDelegate(address) + }, + [], + ) + + const handleDelegateReset = useCallback(() => { + setSelectedPublicDelegate(undefined) + }, []) + + return ( + + {children} + + ) +} diff --git a/modules/delegation/providers/DelegationFormContext.tsx b/modules/delegation/providers/DelegationFormContext.tsx index fd670130..4d890670 100644 --- a/modules/delegation/providers/DelegationFormContext.tsx +++ b/modules/delegation/providers/DelegationFormContext.tsx @@ -5,6 +5,7 @@ import { useContext, useCallback, useState, + useEffect, } from 'react' import { useGovernanceBalance } from 'modules/tokens/hooks/useGovernanceBalance' import { useDelegationInfo } from '../hooks/useDelegationInfo' @@ -19,6 +20,8 @@ import { import { useDelegationFormSubmit } from '../hooks/useDelegationFormSubmit' import { useDelegationRevoke } from '../hooks/useDelegationRevoke' import { ToastSuccess } from '@lidofinance/lido-ui' +import { isValidAddress } from 'modules/shared/utils/addressValidation' +import { useDelegateFromPublicList } from './DelegateFromPublicListContext' // // Data context @@ -116,17 +119,39 @@ const useDelegationFormActions = ( // // Data provider // -export const DelegationFormProvider: FC<{ mode: DelegationFormMode }> = ({ +export type DelegationFormProviderProps = { + mode: DelegationFormMode +} + +export const DelegationFormProvider: FC = ({ children, mode, }) => { const networkData = useDelegationFormNetworkData() + const { selectedPublicDelegate, onPublicDelegateReset } = + useDelegateFromPublicList() const formObject = useForm({ - defaultValues: { delegateAddress: null }, + defaultValues: { delegateAddress: '' }, mode: 'onChange', }) + useEffect(() => { + const currentValue = formObject.getValues('delegateAddress') + if ( + selectedPublicDelegate && + isValidAddress(selectedPublicDelegate) && + currentValue?.toLowerCase() !== selectedPublicDelegate.toLowerCase() + ) { + formObject.setValue('delegateAddress', selectedPublicDelegate, { + shouldValidate: true, + }) + formObject.setFocus('delegateAddress') + onPublicDelegateReset() + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedPublicDelegate]) + const { isSubmitting, txAragonDelegate, diff --git a/modules/delegation/publicDelegates.ts b/modules/delegation/publicDelegates.ts new file mode 100644 index 00000000..6c384629 --- /dev/null +++ b/modules/delegation/publicDelegates.ts @@ -0,0 +1,262 @@ +// The list of public delegates was provided by DAO Ops workstream member +export const PUBLIC_DELEGATES = [ + { + name: 'Matt Stam', + avatar: + 'https://dub1.discourse-cdn.com/business20/user_avatar/research.lido.fi/mattstam/288/2888_2.png', + address: '0x33379367200Ac200182ccD4abD71683F2D24e373', + lido: 'https://research.lido.fi/t/matt-stam-delegate-thread/8145', + twitter: 'https://x.com/mattstam_eth', + }, + { + name: 'Ignas', + avatar: + 'https://dub1.discourse-cdn.com/business20/user_avatar/research.lido.fi/ignas/288/4798_2.png', + address: '0x3DDC7d25c7a1dc381443e491Bbf1Caa8928A05B0', + lido: 'https://research.lido.fi/t/ignas-delegate-thread/8135', + twitter: 'https://x.com/DefiIgnas', + }, + { + name: 'Wintermute Governance', + avatar: + 'https://dub1.discourse-cdn.com/business20/user_avatar/research.lido.fi/wintermute/288/3963_2.png', + address: '0xB933AEe47C438f22DE0747D57fc239FE37878Dd1', + lido: 'https://research.lido.fi/t/wintermute-governance-delegate-thread/8131', + twitter: 'https://x.com/wintermute_t', + }, + { + name: 'WPRC (WhitePaper Reading Club)', + avatar: + 'https://images.squarespace-cdn.com/content/v1/65be10b23014452b13c31611/af6739ef-4a80-4175-8d04-eb7f7a5e260f/IMG_2592.png?format=1500w', + address: '0x8Ab6612BbcF7E133A6BB03b3264718d30f25e0BA', + lido: 'https://research.lido.fi/t/whitepaper-reading-club-delegate-thread/8100', + twitter: 'https://x.com/wpreadingclub', + }, + { + name: 'Tane', + avatar: + 'https://europe1.discourse-cdn.com/business20/uploads/lido/original/2X/c/c24f99b7e88e3edac9cb5a09a5cbf1ee55c54b1c.png', + address: '0xB79294D00848a3A4C00c22D9367F19B4280689D7', + lido: 'https://research.lido.fi/t/tane-delegate-thread/7639', + twitter: 'https://x.com/tanelabs', + }, + { + name: 'SEEDOrg', + avatar: + 'https://dub1.discourse-cdn.com/business20/user_avatar/research.lido.fi/seedorg/288/4048_2.png', + address: '0xc1c2e8a21b86e41d1e706c232a2db5581b3524f8', + lido: 'https://research.lido.fi/t/seedgov-delegate-thread/7643', + twitter: 'https://x.com/SEEDGov', + }, + { + name: 'Anthony Leuts', + avatar: + 'https://dub1.discourse-cdn.com/business20/user_avatar/research.lido.fi/leuts/288/4427_2.png', + address: '0x42E6DD8D517abB3E4f6611Ca53a8D1243C183fB0', + lido: 'https://research.lido.fi/t/anthony-leuts-delegate-thread/8019', + twitter: 'https://x.com/A_Leutenegger', + }, + { + name: 'Sov', + avatar: + 'https://dub1.discourse-cdn.com/business20/user_avatar/research.lido.fi/sov/288/4684_2.png', + address: 'sovereignsignal.eth', + lido: 'https://research.lido.fi/t/sov-delegate-thread/8069', + twitter: 'https://x.com/sovereignsignal', + }, + { + name: 'Polar', + avatar: + 'https://dub1.discourse-cdn.com/business20/user_avatar/research.lido.fi/polar/288/4806_2.png', + address: '0x1f76a6Bf03429480472B3695E08689219cE15ED6', + lido: 'https://research.lido.fi/t/polar-delegate-thread/8048', + twitter: 'https://x.com/post_polar_', + }, + { + name: 'TokenLogic', + avatar: + 'https://dub1.discourse-cdn.com/business20/user_avatar/research.lido.fi/tokenlogic/288/2945_2.png', + address: '0x2cc1ADE245020FC5AAE66Ad443e1F66e01c54Df1', + lido: 'https://research.lido.fi/t/tokenlogic-delegate-thread/8066', + twitter: 'https://x.com/Token_Logic', + }, + { + name: 'BlockworksResearch', + avatar: + 'https://europe1.discourse-cdn.com/business20/uploads/lido/original/2X/e/eb38115f63cd4faf2565e776b3aa110564d76132.jpeg', + address: '0xfF4139e99Bd7c23F4611dc660c33c97A825EA67b', + lido: 'https://research.lido.fi/t/blockworks-research-delegate-thread/8024', + twitter: 'https://x.com/blockworksres', + }, + { + name: 'Blockful (Daniela Zschaber)', + avatar: + 'https://dub1.discourse-cdn.com/business20/user_avatar/research.lido.fi/danimim/288/4785_2.png', + address: '0x3b9F47629cD4D5903cF3eB897aaC4F6b41Dd2589', + lido: 'https://research.lido.fi/t/daniela-zschaber-representing-blockful-delegate-thread/8018', + twitter: 'https://x.com/danimimm', + }, + { + name: 'karpatkey', + avatar: + 'https://dub1.discourse-cdn.com/business20/user_avatar/research.lido.fi/karpatkey/288/2307_2.png', + address: '0x8787FC2De4De95c53e5E3a4e5459247D9773ea52', + lido: 'https://research.lido.fi/t/karpatkey-delegate-thread/8011', + twitter: 'https://x.com/karpatkey', + }, + { + name: 'StableLab', + avatar: + 'https://dub1.discourse-cdn.com/business20/user_avatar/research.lido.fi/stable_lab/288/3053_2.png', + address: 'stablelab.eth', + lido: 'https://research.lido.fi/t/stablelab-delegate-thread-updated/4904/17', + twitter: 'https://x.com/Stablelab', + }, + { + name: 'DegentradingLSD', + avatar: null, + address: 'degentradinglsd3.eth', + lido: 'https://research.lido.fi/t/degentradinglsd-delegate-thread/8149', + twitter: 'https://x.com/degentradingLSD', + }, + { + name: 'NodeSoda', + avatar: + 'https://dub1.discourse-cdn.com/business20/user_avatar/research.lido.fi/fible1/288/4854_2.png', + address: '0x000b4369B71B6634F27F5De9Cbaaabb0D21B8be5', + lido: 'https://research.lido.fi/t/nodesoda-com-delegate-thread/8031', + twitter: 'https://x.com/pablolema85', + }, + { + name: 'eboadom (Ernesto)', + avatar: + 'https://dub1.discourse-cdn.com/business20/user_avatar/research.lido.fi/eboadom/288/3764_2.png', + address: '0x5Ef980c7bdA50c81E8FB13DfF2b804113065ED1c', + lido: 'https://research.lido.fi/t/eboadom-delegate-thread/8079', + twitter: 'https://x.com/eboadom', + }, + { + name: 'ReservoirDAO', + avatar: + 'https://dub1.discourse-cdn.com/business20/user_avatar/research.lido.fi/sharp/288/4749_2.png', + address: '0x4f894Bfc9481110278C356adE1473eBe2127Fd3C', + lido: 'https://research.lido.fi/t/reservoirdao-alphagrowth-delegate-platform/8169', + twitter: 'https://x.com/alphagrowth1', + }, + { + name: 'FranklinDAO', + avatar: + 'https://dub1.discourse-cdn.com/business20/user_avatar/research.lido.fi/franklindao/288/4879_2.png', + address: '0x070341aA5Ed571f0FB2c4a5641409B1A46b4961b', + lido: 'https://research.lido.fi/t/franklin-dao-delegate-platform/8167', + twitter: 'https://x.com/franklin_dao', + }, + { + name: 'Mog', + avatar: + 'https://dub1.discourse-cdn.com/business20/user_avatar/research.lido.fi/mogglet/288/4834_2.png', + address: '0x61Cc8f06F3B6e1a3Fba28CbEbc4c89304b1187D1', + lido: 'https://research.lido.fi/t/mog-delegate-thread/8164', + twitter: 'https://x.com/tungtdinh', + }, + { + name: 'Pol Lanski', + avatar: + 'https://dub1.discourse-cdn.com/business20/user_avatar/research.lido.fi/lanski/288/4846_2.png', + address: 'lanski.eth', + lido: 'https://research.lido.fi/t/pol-lanski-delegate-thread/8155', + twitter: 'https://x.com/Pol_Lanski', + }, + { + name: 'notjamiedimon', + avatar: + 'https://pbs.twimg.com/profile_images/1821464105515163648/ddtnBp5f_400x400.jpg', + address: 'notjamiedimon.eth', + lido: 'https://research.lido.fi/t/notjamiedimon-delegate-thread/8174', + twitter: 'https://x.com/regentrading', + }, + { + name: 'Irina', + avatar: + 'https://dub1.discourse-cdn.com/business20/user_avatar/research.lido.fi/irina_everstake/288/2107_2.png', + address: '0x06A90756e57bC7A016Eed0aB23fC36d11C42bBa0', + lido: 'https://research.lido.fi/t/irina-delegate-thread/8217', + twitter: 'https://x.com/eth_everstake', + }, + { + name: 'marcbcs', + avatar: + 'https://dub1.discourse-cdn.com/business20/user_avatar/research.lido.fi/marcbcs/288/2659_2.png', + address: '0x98308b6dA79B47D15e9438CB66831563649Dbd94', + lido: 'https://research.lido.fi/t/marcbcs-delegate-thread/8209', + twitter: 'https://x.com/marcbcs', + }, + { + name: 'Daedalus Collective', + avatar: + 'https://europe1.discourse-cdn.com/business20/uploads/lido/original/2X/3/34aed82e52916083ae6b83fc17510409371527a6.png', + address: '0x04827a54F2e345467beAFEfB9EF76Cb2f2c62D83', + lido: 'https://research.lido.fi/t/daedalus-delegate-thread/8195', + twitter: 'https://x.com/daedalus_angels', + }, + { + name: 'ProRelGuild', + avatar: null, + address: 'prorelguild.eth', + lido: 'https://research.lido.fi/t/prorelguild-delegate-thread/8186', + twitter: 'https://x.com/apegenija', + }, + { + name: 'Today in DeFi', + avatar: null, + address: '0xf163D77B8EfC151757fEcBa3D463f3BAc7a4D808', + lido: 'https://research.lido.fi/t/today-in-defi-delegate-thread/8207', + twitter: 'https://x.com/todayindefi', + }, + { + name: 'Next Finance Tech', + avatar: + 'https://dub1.discourse-cdn.com/business20/user_avatar/research.lido.fi/nextfinancetech/288/4870_2.png', + address: '0x18c674F655594F15c490aeEac737895b7903E37f', + lido: 'https://research.lido.fi/t/next-finance-tech-delegate-thread/8204', + twitter: 'https://x.com/nxt_fintech', + }, + { + name: 'cp0x', + avatar: + 'https://dub1.discourse-cdn.com/business20/user_avatar/research.lido.fi/cp0x/288/4886_2.png', + address: '0x6f9BB7e454f5B3eb2310343f0E99269dC2BB8A1d', + lido: 'https://research.lido.fi/t/cp0x-delegate-thread/8193', + twitter: 'https://x.com/cp0xdotcom', + }, + { + name: 'Simply Staking', + avatar: + 'https://dub1.discourse-cdn.com/business20/user_avatar/research.lido.fi/damien/288/4853_2.png', + address: '0xCeDF324843775c9E7f695251001531798545614B', + lido: 'https://research.lido.fi/t/simply-staking-delegate-thread/8178', + twitter: 'https://x.com/SimplyStaking', + }, + { + name: 'Boardroom', + avatar: null, + address: 'boardroomgov.eth', + lido: 'https://research.lido.fi/t/boardroom-delegate-thread/8200', + twitter: 'https://x.com/boardroom_info', + }, + { + name: 'Proof Group', + avatar: null, + address: '0xc3A673736415BbF5ba8A8E0642eC3Ab16F4Ada24', + lido: 'https://research.lido.fi/t/proof-group-delegate-thread/8190', + twitter: 'https://x.com/njess', + }, + { + name: 'Lemma Solutions', + avatar: + 'https://dub1.discourse-cdn.com/business20/user_avatar/research.lido.fi/lemma/288/4873_2.png', + address: '0x58B1b454Dbe5156ACc8FC2139E7238451b59f432', + lido: 'https://research.lido.fi/t/lemma-delegate-thread/8214', + twitter: 'https://x.com/Lemma_Solutions', + }, +] diff --git a/modules/delegation/ui/DelegationForm/DelegationAddressInput.tsx b/modules/delegation/ui/DelegationForm/DelegationAddressInput.tsx index 0fc0daca..eb18343d 100644 --- a/modules/delegation/ui/DelegationForm/DelegationAddressInput.tsx +++ b/modules/delegation/ui/DelegationForm/DelegationAddressInput.tsx @@ -12,12 +12,11 @@ export function DelegationAddressInput() { aragonDelegateAddress, snapshotDelegateAddress, mode, - register, } = useDelegationFormData() return ( void } -export function DelegationForm({ mode, onCustomizeClick }: Props) { +export function DelegationForm({ onCustomizeClick, ...providerProps }: Props) { return ( - + diff --git a/modules/delegation/ui/DelegationForm/DelegationFormStyle.ts b/modules/delegation/ui/DelegationForm/DelegationFormStyle.ts index 54e67188..65b656d0 100644 --- a/modules/delegation/ui/DelegationForm/DelegationFormStyle.ts +++ b/modules/delegation/ui/DelegationForm/DelegationFormStyle.ts @@ -1,4 +1,4 @@ -import styled from 'styled-components' +import styled, { css } from 'styled-components' import { Button, Text } from '@lidofinance/lido-ui' export const DelegationFormControllerStyled = styled.form<{ @@ -9,11 +9,11 @@ export const DelegationFormControllerStyled = styled.form<{ ${({ $customMode }) => $customMode && - ` - padding: 24px 16px; - background-color: var(--lido-color-accentControlBg); - border-radius: 20px; - `} + css` + padding: 24px 16px; + background-color: var(--lido-color-accentControlBg); + border-radius: ${({ theme }) => theme.borderRadiusesMap.xl}px; + `} ` export const DelegationSubtitleStyled = styled.div` diff --git a/modules/delegation/ui/DelegationForm/DelegationFormSubmitButton.tsx b/modules/delegation/ui/DelegationForm/DelegationFormSubmitButton.tsx index 77ab2e35..088a3f92 100644 --- a/modules/delegation/ui/DelegationForm/DelegationFormSubmitButton.tsx +++ b/modules/delegation/ui/DelegationForm/DelegationFormSubmitButton.tsx @@ -65,7 +65,7 @@ export function DelegationFormSubmitButton({ onCustomizeClick }: Props) { if (isSimple) { return ` ${match.isRedelegate ? 'Redelegate' : 'Delegate'} - ${!match.isInputMatchAragon || !match.isInputMatchSnapshot ? ' on ' : ''} + ${!match.isInputMatchAragon || !match.isInputMatchSnapshot ? ' on ' : ''} ${match.isInputMatchAragon ? '' : 'Aragon'} ${!match.isInputMatchAragon && !match.isInputMatchSnapshot ? ' & ' : ''} ${match.isInputMatchSnapshot ? '' : 'Snapshot'}` @@ -128,7 +128,7 @@ export function DelegationFormSubmitButton({ onCustomizeClick }: Props) { if (!isWalletConnected) { return ( - Connect wallet to delegate + Connect wallet ) } diff --git a/modules/delegation/ui/DelegationForm/DelegationFormSubtitle.tsx b/modules/delegation/ui/DelegationForm/DelegationFormSubtitle.tsx index 0af0bac9..58b2a616 100644 --- a/modules/delegation/ui/DelegationForm/DelegationFormSubtitle.tsx +++ b/modules/delegation/ui/DelegationForm/DelegationFormSubtitle.tsx @@ -1,4 +1,4 @@ -import { Text } from '@lidofinance/lido-ui' +import { Text, useBreakpoint } from '@lidofinance/lido-ui' import { DelegationSubtitleStyled } from './DelegationFormStyle' import { useDelegationFormData } from 'modules/delegation/providers/DelegationFormContext' @@ -7,12 +7,13 @@ import SnapshotSvg from 'assets/snapshot.com.svg.react' export function DelegationFormSubtitle() { const { mode } = useDelegationFormData() + const isMobile = useBreakpoint('md') if (mode === 'aragon') { return ( - + On Aragon @@ -23,7 +24,7 @@ export function DelegationFormSubtitle() { return ( - + On Snapshot diff --git a/modules/delegation/ui/DelegationSettings/DelegationSettings.tsx b/modules/delegation/ui/DelegationSettings/DelegationSettings.tsx index 03540bdb..5b0e7946 100644 --- a/modules/delegation/ui/DelegationSettings/DelegationSettings.tsx +++ b/modules/delegation/ui/DelegationSettings/DelegationSettings.tsx @@ -1,39 +1,47 @@ import { useState } from 'react' -import { Button, Text } from '@lidofinance/lido-ui' +import { Button, Text, useBreakpoint } from '@lidofinance/lido-ui' -import { FormTitle, Wrap } from './DelegationSettingsStyle' +import { FormTitle, FormWrap, Wrap } from './DelegationSettingsStyle' import { DelegationForm } from '../DelegationForm' +import { PublicDelegateList } from '../PublicDelegateList' +import { DelegateFromPublicListProvider } from '../../providers/DelegateFromPublicListContext' export function DelegationSettings() { const [isSimpleModeOn, setIsSimpleModeOn] = useState(true) + const isMobile = useBreakpoint('md') return ( - - - - Delegation - - {!isSimpleModeOn && ( - - )} - - {isSimpleModeOn ? ( - setIsSimpleModeOn(false)} - /> - ) : ( - <> - - - - )} + + + + + + Delegation + + {!isSimpleModeOn && ( + + )} + + {isSimpleModeOn ? ( + setIsSimpleModeOn(false)} + /> + ) : ( + <> + + + + )} + + + ) } diff --git a/modules/delegation/ui/DelegationSettings/DelegationSettingsStyle.ts b/modules/delegation/ui/DelegationSettings/DelegationSettingsStyle.ts index 2a9ea814..2615b7a5 100644 --- a/modules/delegation/ui/DelegationSettings/DelegationSettingsStyle.ts +++ b/modules/delegation/ui/DelegationSettings/DelegationSettingsStyle.ts @@ -1,18 +1,55 @@ +import { + BREAKPOINT_MD, + BREAKPOINT_MOBILE, + HEADER_HEIGHT, +} from 'modules/globalStyles' import styled from 'styled-components' -export const Wrap = styled.div<{ $customizable: boolean }>` - border-radius: 20px; +export const Wrap = styled.div` + display: flex; + gap: 20px; + justify-content: center; + align-items: flex-start; + flex-wrap: wrap; + + & > div { + flex: 1; + } + + @media (max-width: ${BREAKPOINT_MOBILE}) { + flex-direction: column; + align-items: center; + & > div { + width: 100%; + max-width: 542px; + } + } +` + +export const FormWrap = styled.div<{ $customizable: boolean }>` + border-radius: ${({ theme }) => theme.borderRadiusesMap.xl}px; background-color: var(--lido-color-foreground); display: flex; flex-direction: column; gap: 24px; padding: 32px; + max-width: 496px; + position: sticky; + top: calc(${HEADER_HEIGHT} + 20px); ${({ $customizable }) => $customizable && ` padding: 32px 24px; `} + + @media (max-width: ${BREAKPOINT_MOBILE}) { + position: static; + } + + @media (max-width: ${BREAKPOINT_MD}) { + padding: 20px; + } ` export const FormTitle = styled.div` diff --git a/modules/delegation/ui/DelegatorsList/DelegatorsList.tsx b/modules/delegation/ui/DelegatorsList/DelegatorsList.tsx index dcad7a47..157261b3 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,41 @@ 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..a534d107 --- /dev/null +++ b/modules/delegation/ui/DelegatorsList/DelegatorsListItem.tsx @@ -0,0 +1,38 @@ +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 + ensName: string | null | undefined + governanceSymbol: string | undefined +} + +export function DelegatorsListItem({ + address, + balance, + governanceSymbol, + ensName, +}: Props) { + return ( + + + + + + {ensName ?? trimAddress(address, 6)} + + + + + {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 4bca89ea..44ee7d4e 100644 --- a/modules/delegation/ui/DelegatorsList/DelegatorsListStyle.ts +++ b/modules/delegation/ui/DelegatorsList/DelegatorsListStyle.ts @@ -2,12 +2,15 @@ import { Button } from '@lidofinance/lido-ui' import styled from 'styled-components' export const Wrap = styled.div<{ $empty?: boolean }>` - border-radius: 20px; + border-radius: ${({ theme }) => theme.borderRadiusesMap.xl}px; background-color: var(--lido-color-foreground); padding: 32px; display: flex; flex-direction: column; gap: 12px; + max-width: 496px; + margin: 0 auto; + text-align: center; ${({ $empty }) => $empty && @@ -44,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; diff --git a/modules/delegation/ui/PublicDelegateList/PublicDelegateAvatar.tsx b/modules/delegation/ui/PublicDelegateList/PublicDelegateAvatar.tsx new file mode 100644 index 00000000..c864bdd1 --- /dev/null +++ b/modules/delegation/ui/PublicDelegateList/PublicDelegateAvatar.tsx @@ -0,0 +1,30 @@ +import Image from 'next/image' +import { AvatarWrap } from './PublicDelegateListStyle' + +import AvatarSvg from 'assets/avatar.com.svg.react' + +type Props = { + avatarSrc: string | null | undefined +} + +export function PublicDelegateAvatar({ avatarSrc }: Props) { + if (!avatarSrc) { + return ( + + + + ) + } + + return ( + + src} + unoptimized + /> + + ) +} diff --git a/modules/delegation/ui/PublicDelegateList/PublicDelegateList.tsx b/modules/delegation/ui/PublicDelegateList/PublicDelegateList.tsx new file mode 100644 index 00000000..c3ef8259 --- /dev/null +++ b/modules/delegation/ui/PublicDelegateList/PublicDelegateList.tsx @@ -0,0 +1,68 @@ +import { Text, Tooltip, useBreakpoint } from '@lidofinance/lido-ui' +import { + Header, + HeaderTitleWithIcon, + InnerWrap, + Wrap, +} from './PublicDelegateListStyle' +import { useWeb3 } from 'modules/blockChain/hooks/useWeb3' +import { useProcessedPublicDelegatesList } from './useProcessedPublicDelegatesList' +import { PageLoader } from 'modules/shared/ui/Common/PageLoader' +import { PublicDelegateListItem } from './PublicDelegateListItem' + +import AragonSvg from 'assets/aragon.com.svg.react' + +export function PublicDelegateList() { + const { isWalletConnected } = useWeb3() + const isMobile = useBreakpoint('md') + + const { data, initialLoading } = useProcessedPublicDelegatesList() + + if (!data || initialLoading) { + return ( + + + + ) + } + + return ( + + + Public Delegate List + + + {!isMobile && ( +
+ + Delegate + + + + VP + + + + + From + + +

+

+

+ )} + {data.map(delegate => ( + + ))} +
+
+ ) +} diff --git a/modules/delegation/ui/PublicDelegateList/PublicDelegateListItem.tsx b/modules/delegation/ui/PublicDelegateList/PublicDelegateListItem.tsx new file mode 100644 index 00000000..e1a4a925 --- /dev/null +++ b/modules/delegation/ui/PublicDelegateList/PublicDelegateListItem.tsx @@ -0,0 +1,132 @@ +import { + DelegateInfo, + DelegateNameAndAddress, + DelegateNumbersMobile, + HeaderTitleWithIcon, + ListItem, + SocialButtons, +} from './PublicDelegateListStyle' +import { ProcessedDelegate } from './useProcessedPublicDelegatesList' +import { Button, Text, trimAddress } from '@lidofinance/lido-ui' +import { AddressPop } from 'modules/shared/ui/Common/AddressPop' +import { isValidAddress } from 'modules/shared/utils/addressValidation' +import { formatBalance } from 'modules/blockChain/utils/formatBalance' +import { ExternalLink } from 'modules/shared/ui/Common/ExternalLink' +import { PublicDelegateAvatar } from './PublicDelegateAvatar' +import { useDelegateFromPublicList } from 'modules/delegation/providers/DelegateFromPublicListContext' + +import XSocialSvg from 'assets/x.social.com.svg.react' +import LidoSocialSvg from 'assets/lido.social.com.svg.react' +import AragonSvg from 'assets/aragon.com.svg.react' + +type Props = { + delegate: ProcessedDelegate + isWalletConnected: boolean + isMobile: boolean +} + +export function PublicDelegateListItem({ + delegate, + isWalletConnected, + isMobile, +}: Props) { + const { onPublicDelegateSelect } = useDelegateFromPublicList() + + const addressToShow = isValidAddress(delegate.address) + ? trimAddress(delegate.address, 6) + : delegate.address + + const balanceToShow = + delegate.delegatedVotingPower === 'N/A' + ? delegate.delegatedVotingPower + : formatBalance(delegate.delegatedVotingPower) + + if (isMobile) { + return ( + + + + + + {delegate.name} + + + + {addressToShow} + + + + + + + + {delegate.twitter && ( + + + + )} + + + + + VP + {balanceToShow} + + + From {delegate.delegatorsCount} + + + {isWalletConnected && ( + + )} + + ) + } + + return ( + + + + + + {delegate.name} + + + + {addressToShow} + + + + + {balanceToShow} + {delegate.delegatorsCount} + + + + + {delegate.twitter && ( + + + + )} + + {isWalletConnected && ( + + )} + + ) +} diff --git a/modules/delegation/ui/PublicDelegateList/PublicDelegateListStyle.ts b/modules/delegation/ui/PublicDelegateList/PublicDelegateListStyle.ts new file mode 100644 index 00000000..9cc0298d --- /dev/null +++ b/modules/delegation/ui/PublicDelegateList/PublicDelegateListStyle.ts @@ -0,0 +1,163 @@ +import { Text } from '@lidofinance/lido-ui' +import { BREAKPOINT_MOBILE, BREAKPOINT_MD } from 'modules/globalStyles' +import styled from 'styled-components' + +export const Wrap = styled.div` + padding: 32px 24px; + border-radius: ${({ theme }) => theme.borderRadiusesMap.xl}px; + background-color: var(--lido-color-foreground); + display: flex; + flex-direction: column; + gap: 24px; + max-width: 542px; + min-width: 460px; + + @media (max-width: ${BREAKPOINT_MOBILE}) { + min-width: unset; + max-height: unset; + } + + @media (max-width: ${BREAKPOINT_MD}) { + padding: 20px; + } +` + +export const InnerWrap = styled.div<{ $connected: boolean }>` + border-radius: inherit; + border: 1px solid var(--lido-color-border); + display: flex; + flex-direction: column; + overflow-x: hidden; + + & > div { + display: grid; + gap: 10px; + grid-template-columns: minmax(130px, 3fr) repeat(2, minmax(42px, 1fr)) 40px ${({ + $connected, + }) => ($connected ? 'minmax(0, 85px)' : '')}; + + @media (max-width: ${BREAKPOINT_MD}) { + display: flex; + } + } +` + +export const Header = styled.div` + padding: 24px 12px 16px 12px; + position: sticky; + top: 0; + background-color: var(--lido-color-foreground); + z-index: 2; + border-bottom: 1px solid var(--lido-color-border); +` + +export const HeaderTitleWithIcon = styled(Text).attrs({ + size: 'xxs', + weight: 700, +})` + display: flex; + align-items: center; + gap: 4px; +` + +export const ListItem = styled.div` + align-items: center; + padding: 12px; + + &:nth-child(odd) { + background-color: var(--lido-color-background); + } + + @media (max-width: ${BREAKPOINT_MD}) { + padding: 20px 12px; + display: flex; + flex-direction: column; + gap: 12px; + align-items: stretch; + } +` +export const DelegateInfo = styled.div` + display: flex; + align-items: center; + gap: 8px; + + @media (max-width: ${BREAKPOINT_MD}) { + gap: 12px; + } +` + +export const AvatarWrap = styled.div` + position: relative; + border-radius: 50%; + width: 28px; + height: 28px; + overflow: hidden; + flex-shrink: 0; + + & > img, + & > svg { + width: 100%; + height: 100%; + } + + & > img[src=''] { + display: none; + } + + @media (max-width: ${BREAKPOINT_MD}) { + width: 32px; + height: 32px; + } +` + +export const SocialButtons = styled.div` + display: flex; + gap: 6px; + + & > span { + height: 16px; + + & > svg path { + transition: fill ease ${({ theme }) => theme.duration.norm}; + } + + &:hover > svg path { + fill: var(--lido-color-primaryHover); + } + } + + @media (max-width: ${BREAKPOINT_MD}) { + margin-left: auto; + & > span { + height: 24px; + } + + svg { + width: 24px; + height: 24px; + } + } +` + +export const DelegateNameAndAddress = styled.div` + max-width: calc(100% - 36px); + + & > p, + & > span { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; + } + + @media (max-width: ${BREAKPOINT_MD}) { + max-width: calc(100% - 110px); + flex: 1; + } +` + +export const DelegateNumbersMobile = styled.div` + display: inline-flex; + align-items: center; + gap: 32px; +` diff --git a/modules/delegation/ui/PublicDelegateList/index.ts b/modules/delegation/ui/PublicDelegateList/index.ts new file mode 100644 index 00000000..3d509fea --- /dev/null +++ b/modules/delegation/ui/PublicDelegateList/index.ts @@ -0,0 +1 @@ +export { PublicDelegateList } from './PublicDelegateList' diff --git a/modules/delegation/ui/PublicDelegateList/useProcessedPublicDelegatesList.ts b/modules/delegation/ui/PublicDelegateList/useProcessedPublicDelegatesList.ts new file mode 100644 index 00000000..bdd3b7c9 --- /dev/null +++ b/modules/delegation/ui/PublicDelegateList/useProcessedPublicDelegatesList.ts @@ -0,0 +1,111 @@ +import { useLidoSWRImmutable } from '@lido-sdk/react' +import { BigNumber } from 'ethers' +import { ContractVoting } from 'modules/blockChain/contracts' +import { useWeb3 } from 'modules/blockChain/hooks/useWeb3' +import { DELEGATORS_FETCH_TOTAL } from 'modules/delegation/constants' +import { PUBLIC_DELEGATES } from 'modules/delegation/publicDelegates' +import { useEnsResolvers } from 'modules/shared/hooks/useEnsResolvers' +import { + isValidAddress, + isValidEns, +} from 'modules/shared/utils/addressValidation' + +export type ProcessedDelegate = typeof PUBLIC_DELEGATES[number] & { + delegatorsCount: string + delegatedVotingPower: BigNumber | string + resolvedDelegateAddress: string | null +} + +export const useProcessedPublicDelegatesList = () => { + const { chainId } = useWeb3() + const voting = ContractVoting.useRpc() + + const { resolveName } = useEnsResolvers() + + return useLidoSWRImmutable( + [`swr:useProcessedPublicDelegatesList`, chainId], + async () => { + const parsedList: ProcessedDelegate[] = await Promise.all( + PUBLIC_DELEGATES.map(async delegate => { + let resolvedDelegateAddress: string | null = + delegate.address.toLowerCase() + + // If `address` was provided as ENS name, convert it to address + if (isValidEns(delegate.address)) { + resolvedDelegateAddress = + (await resolveName(delegate.address))?.toLowerCase() ?? null + + // If ENS name wasn't not resolved, return delegate with N/A values + if (!resolvedDelegateAddress) { + return { + ...delegate, + delegatorsCount: 'N/A', + delegatedVotingPower: 'N/A', + resolvedDelegateAddress: null, + } + } + } else if (!isValidAddress(delegate.address)) { + return { + ...delegate, + delegatorsCount: 'N/A', + delegatedVotingPower: 'N/A', + resolvedDelegateAddress: null, + } + } + + const delegatorsCount = await voting.getDelegatedVotersCount( + resolvedDelegateAddress, + ) + + if (delegatorsCount.isZero()) { + return { + ...delegate, + delegatorsCount: '0', + delegatedVotingPower: BigNumber.from(0), + resolvedDelegateAddress, + } + } + + const delegatorsAddresses = await voting.getDelegatedVoters( + resolvedDelegateAddress, + 0, + DELEGATORS_FETCH_TOTAL, + ) + const delegatorsBalances = await voting.getVotingPowerMultiple( + delegatorsAddresses, + ) + const delegatedVotingPower = delegatorsBalances.reduce( + (acc, balance) => acc.add(balance), + BigNumber.from(0), + ) + + return { + ...delegate, + delegatorsCount: delegatorsCount.toString(), + delegatedVotingPower: delegatedVotingPower, + resolvedDelegateAddress, + } + }), + ) + + return parsedList.sort((a, b) => { + if (typeof a.delegatedVotingPower === 'string') { + return 1 + } + if (typeof b.delegatedVotingPower === 'string') { + return -1 + } + if (a.delegatedVotingPower.lt(b.delegatedVotingPower)) { + return 1 + } + if (a.delegatedVotingPower.gt(b.delegatedVotingPower)) { + return -1 + } + return 0 + }) + }, + { + onError: (error, key) => console.error(key, error), + }, + ) +} diff --git a/modules/globalStyles/index.ts b/modules/globalStyles/index.ts index 4f6ac5eb..e930d7d1 100644 --- a/modules/globalStyles/index.ts +++ b/modules/globalStyles/index.ts @@ -1,6 +1,9 @@ +import { themeDefault } from '@lidofinance/lido-ui' import { createGlobalStyle } from 'styled-components' export const BREAKPOINT_MOBILE = '960px' +export const BREAKPOINT_MD = themeDefault.breakpointsMap.md.width +export const HEADER_HEIGHT = '76px' export const GlobalStyle = createGlobalStyle` *, diff --git a/modules/shared/hooks/useEnsResolvers.ts b/modules/shared/hooks/useEnsResolvers.ts new file mode 100644 index 00000000..03555ac7 --- /dev/null +++ b/modules/shared/hooks/useEnsResolvers.ts @@ -0,0 +1,46 @@ +import { CHAINS } from '@lido-sdk/constants' +import { getStaticRpcBatchProvider } from '@lido-sdk/providers' +import { useWeb3 } from 'modules/blockChain/hooks/useWeb3' +import { useConfig } from 'modules/config/hooks/useConfig' +import { useCallback, useMemo } from 'react' + +export const useEnsResolvers = () => { + const { chainId } = useWeb3() + const { getRpcUrl } = useConfig() + + const ethProvider = useMemo(() => { + const rpcUrl = getRpcUrl(chainId) + return getStaticRpcBatchProvider(chainId, rpcUrl) + }, [chainId, getRpcUrl]) + + const lookupAddress = useCallback( + async (address: string) => { + // ENS name is not supported on Holesky for our current setup + // TODO: revisit this after package upgrade + if (chainId === CHAINS.Holesky) { + return null + } + + return ethProvider.lookupAddress(address) + }, + [chainId, ethProvider], + ) + + const resolveName = useCallback( + async (address: string) => { + // ENS name is not supported on Holesky for our current setup + // TODO: revisit this after package upgrade + if (chainId === CHAINS.Holesky) { + return null + } + + return ethProvider.resolveName(address) + }, + [chainId, ethProvider], + ) + + return { + lookupAddress, + resolveName, + } +} diff --git a/modules/shared/ui/Common/AddressPop/AddressPopStyle.ts b/modules/shared/ui/Common/AddressPop/AddressPopStyle.ts index 649a05fe..d286ea3d 100644 --- a/modules/shared/ui/Common/AddressPop/AddressPopStyle.ts +++ b/modules/shared/ui/Common/AddressPop/AddressPopStyle.ts @@ -46,5 +46,7 @@ export const BadgeWrap = styled.div` font-size: ${({ theme }) => theme.fontSizesMap.xs}px; font-weight: 500; background-color: rgba(0, 0, 0, 0.05); + display: flex; + justify-content: space-between; } ` diff --git a/modules/shared/ui/Common/ButtonExternalView/ButtonExternalView.tsx b/modules/shared/ui/Common/ButtonExternalView/ButtonExternalView.tsx index 19e29bca..daa12137 100644 --- a/modules/shared/ui/Common/ButtonExternalView/ButtonExternalView.tsx +++ b/modules/shared/ui/Common/ButtonExternalView/ButtonExternalView.tsx @@ -23,7 +23,7 @@ export function ButtonExternalView({ onClick={handleClick} icon={} size="xs" - variant="translucent" + variant="ghost" children={children} {...rest} /> diff --git a/modules/shared/ui/Common/CopyOpenActions/CopyOpenActions.tsx b/modules/shared/ui/Common/CopyOpenActions/CopyOpenActions.tsx index 55ce9a77..28ae1bda 100644 --- a/modules/shared/ui/Common/CopyOpenActions/CopyOpenActions.tsx +++ b/modules/shared/ui/Common/CopyOpenActions/CopyOpenActions.tsx @@ -24,7 +24,7 @@ export function CopyOpenActions({ value, entity }: Props) { onClick={handleCopy} icon={} size="xs" - variant="translucent" + variant="ghost" children={`Copy ${copyText}`} data-testid="copyAddressBtn" /> diff --git a/modules/shared/ui/Layout/Header/HeaderStyle.ts b/modules/shared/ui/Layout/Header/HeaderStyle.ts index 2fa9d5df..626c6ff4 100644 --- a/modules/shared/ui/Layout/Header/HeaderStyle.ts +++ b/modules/shared/ui/Layout/Header/HeaderStyle.ts @@ -1,6 +1,6 @@ import styled, { css, keyframes } from 'styled-components' import { Container, Text } from '@lidofinance/lido-ui' -import { BREAKPOINT_MOBILE } from 'modules/globalStyles' +import { BREAKPOINT_MOBILE, HEADER_HEIGHT } from 'modules/globalStyles' export const Wrap = styled(Container).attrs({ as: 'header', @@ -12,7 +12,7 @@ export const Wrap = styled(Container).attrs({ right: 0; padding: 0 ${({ theme }) => theme.spaceMap.lg}px; display: flex; - height: 76px; + height: ${HEADER_HEIGHT}; align-items: center; justify-content: space-between; background-color: var(--lido-color-foreground); @@ -123,10 +123,6 @@ export const InputWrap = styled.div` @media (max-width: 1060px) { width: 200px; } - - @media (max-width: 810px) { - width: 200px; - } ` export const Network = styled.div` @@ -289,6 +285,6 @@ export const MobileSpacer = styled.div` ` export const HeaderSpacer = styled.div` - height: 76px; + height: ${HEADER_HEIGHT}; margin-bottom: 30px; ` diff --git a/modules/shared/utils/addressValidation.ts b/modules/shared/utils/addressValidation.ts new file mode 100644 index 00000000..cb092e6a --- /dev/null +++ b/modules/shared/utils/addressValidation.ts @@ -0,0 +1,8 @@ +import { ethers } from 'ethers' + +const regex = new RegExp('[-a-zA-Z0-9@._]{1,256}.eth') + +export const isValidAddress = (address: string) => + ethers.utils.isAddress(address) + +export const isValidEns = (ens: string) => regex.test(ens) diff --git a/pages/delegation/[[...mode]].tsx b/pages/delegation/[[...mode]].tsx index 04b1ec98..c1a50534 100644 --- a/pages/delegation/[[...mode]].tsx +++ b/pages/delegation/[[...mode]].tsx @@ -9,7 +9,7 @@ import { const DelegationPage: FC = ({ mode }) => { return ( - + Delegation | Lido DAO Voting UI