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