diff --git a/src/app/account/page.tsx b/src/app/account/page.tsx
index 8b094e1..1c3811b 100644
--- a/src/app/account/page.tsx
+++ b/src/app/account/page.tsx
@@ -8,6 +8,9 @@ import { TabHeaderButton } from 'src/components/buttons/TabHeaderButton';
import { Section } from 'src/components/layout/Section';
import { Amount, formatNumberString } from 'src/components/numbers/Amount';
import { useBalance } from 'src/features/account/hooks';
+import { DelegationsTable } from 'src/features/delegation/DelegationsTable';
+import { DelegationAmount } from 'src/features/delegation/types';
+import { useDelegationBalances } from 'src/features/delegation/useDelegationBalances';
import { LockActionType, LockedBalances } from 'src/features/locking/types';
import { useLockedStatus } from 'src/features/locking/useLockedStatus';
import { getTotalLockedCelo, getTotalUnlockedCelo } from 'src/features/locking/utils';
@@ -37,19 +40,23 @@ export default function Page() {
const { balance: walletBalance } = useBalance(address);
const { lockedBalances } = useLockedStatus(address);
+ const { delegations } = useDelegationBalances(address);
const { stakeBalances, groupToStake, refetch: refetchStakes } = useStakingBalances(address);
const { totalRewardsWei, groupToReward } = useStakingRewards(address, groupToStake);
const { groupToIsActivatable, refetch: refetchActivations } = usePendingStakingActivations(
address,
groupToStake,
);
+ const { addressToGroup } = useValidatorGroups();
+
const { activateStake } = useActivateStake(() => {
refetchStakes();
refetchActivations();
});
- const { addressToGroup } = useValidatorGroups();
- const totalBalance = (walletBalance || 0n) + getTotalLockedCelo(lockedBalances);
+ const totalLocked = getTotalLockedCelo(lockedBalances);
+ const totalBalance = (walletBalance || 0n) + totalLocked;
+ const totalDelegated = (delegations?.percentDelegated || 0n) * totalLocked;
return (
@@ -66,6 +73,7 @@ export default function Page() {
lockedBalances={lockedBalances}
stakeBalances={stakeBalances}
totalRewards={totalRewardsWei}
+ totalDelegated={totalDelegated}
/>
@@ -117,11 +126,13 @@ function AccountStats({
lockedBalances,
stakeBalances,
totalRewards,
+ totalDelegated,
}: {
walletBalance?: bigint;
lockedBalances?: LockedBalances;
stakeBalances?: StakingBalances;
totalRewards?: bigint;
+ totalDelegated?: bigint;
}) {
return (
@@ -129,7 +140,7 @@ function AccountStats({
title="Total locked"
valueWei={lockedBalances?.locked}
subtitle="Delegated"
- subValueWei={0n}
+ subValueWei={totalDelegated}
/>
;
groupToReward?: AddressTo;
groupToIsActivatable?: AddressTo;
+ delegateeToAmount?: AddressTo;
activateStake: (g: Address) => void;
}) {
const [tab, setTab] = useState<'stakes' | 'rewards' | 'delegations'>('stakes');
@@ -211,7 +224,7 @@ function TableTabs({
{tab === 'rewards' && (
)}
- {tab === 'delegations' && TODO
}
+ {tab === 'delegations' && }
);
}
diff --git a/src/config/consts.ts b/src/config/consts.ts
index fdf1f2c..9873625 100644
--- a/src/config/consts.ts
+++ b/src/config/consts.ts
@@ -19,3 +19,6 @@ export const PROPOSAL_V1_MAX_ID = 110; // Proposals before this use old vote eve
export const QUEUED_STAGE_EXPIRY_TIME = 2_419_200_000; // 4 weeks
export const APPROVAL_STAGE_EXPIRY_TIME = 86_400_000; // 1 day
export const EXECUTION_STAGE_EXPIRY_TIME = 259_200_000; // 3 days
+
+// Delegation
+export const MAX_NUM_DELEGATEES = 10;
diff --git a/src/features/delegation/DelegationsTable.tsx b/src/features/delegation/DelegationsTable.tsx
new file mode 100644
index 0000000..6e49ac4
--- /dev/null
+++ b/src/features/delegation/DelegationsTable.tsx
@@ -0,0 +1,97 @@
+import { useMemo } from 'react';
+import { FullWidthSpinner } from 'src/components/animation/Spinner';
+import { SolidButton } from 'src/components/buttons/SolidButton';
+import { PLACEHOLDER_BAR_CHART_ITEM, StackedBarChart } from 'src/components/charts/StackedBarChart';
+import { sortAndCombineChartData } from 'src/components/charts/chartData';
+import { HeaderAndSubheader } from 'src/components/layout/HeaderAndSubheader';
+import { formatNumberString } from 'src/components/numbers/Amount';
+import { DelegationAmount } from 'src/features/delegation/types';
+import { TransactionFlowType } from 'src/features/transactions/TransactionFlowType';
+import { useTransactionModal } from 'src/features/transactions/TransactionModal';
+import { ValidatorGroupLogoAndName } from 'src/features/validators/ValidatorGroupLogo';
+import { ValidatorGroup } from 'src/features/validators/types';
+import { tableClasses } from 'src/styles/common';
+import { fromWei } from 'src/utils/amount';
+import { bigIntSum, percent } from 'src/utils/math';
+import { objKeys, objLength } from 'src/utils/objects';
+
+// TODO pass in addressToDelegatee data here
+export function DelegationsTable({
+ delegateeToAmount,
+ addressToDelegatee,
+}: {
+ delegateeToAmount?: AddressTo;
+ addressToDelegatee?: AddressTo;
+}) {
+ const showTxModal = useTransactionModal(TransactionFlowType.Delegate);
+
+ const { chartData, tableData } = useMemo(() => {
+ if (!delegateeToAmount || !objLength(delegateeToAmount)) {
+ return { tableData: [], chartData: [PLACEHOLDER_BAR_CHART_ITEM] };
+ }
+
+ const total = fromWei(
+ bigIntSum(Object.values(delegateeToAmount).map((amount) => amount.expected)),
+ );
+
+ const tableData = objKeys(delegateeToAmount)
+ .map((address) => {
+ const amount = fromWei(delegateeToAmount[address].expected);
+ const percentage = total ? percent(amount, total) : 0;
+ const name = addressToDelegatee?.[address]?.name || 'Unknown Delegatee';
+ return { address, name, amount, percentage };
+ })
+ .sort((a, b) => b.amount - a.amount);
+
+ const chartData = sortAndCombineChartData(
+ tableData.map(({ address, amount, percentage }) => ({
+ label: addressToDelegatee?.[address]?.name || 'Unknown Delegatee',
+ value: amount,
+ percentage,
+ })),
+ );
+ return { chartData, tableData };
+ }, [delegateeToAmount, addressToDelegatee]);
+
+ if (!delegateeToAmount) {
+ return Loading delegation data;
+ }
+
+ if (!objLength(delegateeToAmount)) {
+ return (
+
+ showTxModal()}>Delegate CELO
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ Name |
+ Amount |
+ % |
+
+
+
+ {tableData.map(({ address, name, amount, percentage }) => (
+
+
+
+ |
+ {formatNumberString(amount, 2) + ' CELO'} |
+ {percentage + '%'} |
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/features/delegation/types.ts b/src/features/delegation/types.ts
new file mode 100644
index 0000000..16f2de0
--- /dev/null
+++ b/src/features/delegation/types.ts
@@ -0,0 +1,9 @@
+export interface DelegationAmount {
+ expected: bigint;
+ real: bigint;
+}
+
+export interface DelegationBalances {
+ percentDelegated: bigint;
+ delegateeToAmount: AddressTo;
+}
diff --git a/src/features/delegation/useDelegationBalances.ts b/src/features/delegation/useDelegationBalances.ts
new file mode 100644
index 0000000..63a2d4e
--- /dev/null
+++ b/src/features/delegation/useDelegationBalances.ts
@@ -0,0 +1,88 @@
+import { lockedGoldABI } from '@celo/abis';
+import { useQuery } from '@tanstack/react-query';
+import { useToastError } from 'src/components/notifications/useToastError';
+import { Addresses } from 'src/config/contracts';
+import { DelegationBalances } from 'src/features/delegation/types';
+import { logger } from 'src/utils/logger';
+import { MulticallReturnType, PublicClient } from 'viem';
+import { usePublicClient } from 'wagmi';
+
+export function useDelegationBalances(address?: Address) {
+ const publicClient = usePublicClient();
+
+ const { isLoading, isError, error, data, refetch } = useQuery({
+ queryKey: ['useDelegationBalances', publicClient, address],
+ queryFn: async () => {
+ if (!address || !publicClient) return null;
+ logger.debug('Fetching delegation balances');
+ return fetchDelegationBalances(publicClient, address);
+ },
+ gcTime: 10 * 60 * 1000, // 10 minutes
+ staleTime: 1 * 60 * 1000, // 1 minute
+ });
+
+ useToastError(error, 'Error fetching delegation balances');
+
+ return {
+ isLoading,
+ isError,
+ delegations: data,
+ refetch,
+ };
+}
+
+async function fetchDelegationBalances(
+ publicClient: PublicClient,
+ address: Address,
+): Promise {
+ const result: DelegationBalances = {
+ percentDelegated: 0n,
+ delegateeToAmount: {},
+ };
+
+ // First fetch the list of delegatees addresses
+ const delegateeAddresses = await publicClient.readContract({
+ address: Addresses.LockedGold,
+ abi: lockedGoldABI,
+ functionName: 'getDelegateesOfDelegator',
+ args: [address],
+ });
+
+ // If there are none, stop here
+ if (!delegateeAddresses.length) return result;
+
+ // Fetch the fraction delegated
+ const delegatorPercent = await publicClient.readContract({
+ address: Addresses.LockedGold,
+ abi: lockedGoldABI,
+ functionName: 'getAccountTotalDelegatedFraction',
+ args: [address],
+ });
+ result.percentDelegated = delegatorPercent;
+
+ // Prepare a list of account, delegatee tuples
+ const accountAndDelegatee = delegateeAddresses.map((a) => [address, a]);
+
+ // @ts-ignore TODO Bug with viem 2.0 multicall types
+ const delegatedAmounts: MulticallReturnType = await publicClient.multicall({
+ contracts: accountAndDelegatee.map(([acc, del]) => ({
+ address: Addresses.LockedGold,
+ abi: lockedGoldABI,
+ functionName: 'getDelegatorDelegateeExpectedAndRealAmount',
+ args: [acc, del],
+ })),
+ });
+
+ for (let i = 0; i < delegateeAddresses.length; i++) {
+ const delegateeAddress = delegateeAddresses[i];
+ const amounts = delegatedAmounts[i];
+ if (amounts.status !== 'success') throw new Error('Delegated amount call failed');
+ const [expected, real] = amounts.result as [bigint, bigint];
+ result.delegateeToAmount[delegateeAddress] = {
+ expected,
+ real,
+ };
+ }
+
+ return result;
+}
diff --git a/src/features/staking/ActiveStakesTable.tsx b/src/features/staking/ActiveStakesTable.tsx
index cdda498..22f8033 100644
--- a/src/features/staking/ActiveStakesTable.tsx
+++ b/src/features/staking/ActiveStakesTable.tsx
@@ -48,13 +48,14 @@ export function ActiveStakesTable({
.map((address) => {
const stake = fromWei(groupToStake[address].active + groupToStake[address].pending);
const percentage = percent(stake, total);
- return { address, stake, percentage };
+ const name = addressToGroup?.[address]?.name;
+ return { address, name, stake, percentage };
})
.sort((a, b) => b.stake - a.stake);
const chartData = sortAndCombineChartData(
tableData.map(({ address, stake, percentage }) => ({
- label: addressToGroup[address]?.name || 'Unknown',
+ label: addressToGroup[address]?.name || 'Unknown Group',
value: stake,
percentage,
})),
@@ -91,14 +92,11 @@ export function ActiveStakesTable({
- {tableData.map(({ address, stake, percentage }) => (
+ {tableData.map(({ address, name, stake, percentage }) => (
-
+
{groupToIsActivatable?.[address] && (
Pending
diff --git a/src/features/staking/rewards/RewardsTable.tsx b/src/features/staking/rewards/RewardsTable.tsx
index 0a0cfc8..ff42823 100644
--- a/src/features/staking/rewards/RewardsTable.tsx
+++ b/src/features/staking/rewards/RewardsTable.tsx
@@ -33,13 +33,14 @@ export function RewardsTable({
.map((address) => {
const reward = groupToReward[address];
const percentage = total ? percent(reward, total) : 0;
- return { address, reward, percentage };
+ const name = addressToGroup?.[address]?.name;
+ return { address, name, reward, percentage };
})
.sort((a, b) => b.reward - a.reward);
const chartData = sortAndCombineChartData(
tableData.map(({ address, reward, percentage }) => ({
- label: addressToGroup[address]?.name || 'Unknown',
+ label: addressToGroup[address]?.name || 'Unknown Group',
value: reward,
percentage,
})),
@@ -75,13 +76,10 @@ export function RewardsTable({
|
- {tableData.map(({ address, reward, percentage }) => (
+ {tableData.map(({ address, name, reward, percentage }) => (
-
+
|
{formatNumberString(reward, 2) + ' CELO'} |
{percentage + '%'} |
diff --git a/src/features/staking/useStakingBalances.ts b/src/features/staking/useStakingBalances.ts
index a94a405..93dc5db 100644
--- a/src/features/staking/useStakingBalances.ts
+++ b/src/features/staking/useStakingBalances.ts
@@ -45,7 +45,7 @@ export function useStakingBalances(address?: Address) {
};
}
-export async function fetchStakingBalances(publicClient: PublicClient, address: Address) {
+async function fetchStakingBalances(publicClient: PublicClient, address: Address) {
const groupAddrs = await publicClient.readContract({
address: Addresses.Election,
abi: electionABI,
diff --git a/src/features/validators/ValidatorGroupLogo.tsx b/src/features/validators/ValidatorGroupLogo.tsx
index 951405a..e52b07e 100644
--- a/src/features/validators/ValidatorGroupLogo.tsx
+++ b/src/features/validators/ValidatorGroupLogo.tsx
@@ -40,7 +40,7 @@ export function ValidatorGroupLogoAndName({
- {name || 'Unknown'}
+ {name || 'Unknown Group'}
{shortenAddress(address)}