Skip to content

Commit

Permalink
Implement delegation fetching and table
Browse files Browse the repository at this point in the history
  • Loading branch information
jmrossy committed Mar 2, 2024
1 parent 5b55410 commit 8b16de9
Show file tree
Hide file tree
Showing 9 changed files with 226 additions and 20 deletions.
21 changes: 17 additions & 4 deletions src/app/account/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 (
<Section className="mt-6" containerClassName="space-y-6 px-4">
Expand All @@ -66,13 +73,15 @@ export default function Page() {
lockedBalances={lockedBalances}
stakeBalances={stakeBalances}
totalRewards={totalRewardsWei}
totalDelegated={totalDelegated}
/>
<LockButtons className="flex justify-between md:hidden" />
<TableTabs
groupToStake={groupToStake}
addressToGroup={addressToGroup}
groupToReward={groupToReward}
groupToIsActivatable={groupToIsActivatable}
delegateeToAmount={delegations?.delegateeToAmount}
activateStake={activateStake}
/>
</Section>
Expand Down Expand Up @@ -117,19 +126,21 @@ function AccountStats({
lockedBalances,
stakeBalances,
totalRewards,
totalDelegated,
}: {
walletBalance?: bigint;
lockedBalances?: LockedBalances;
stakeBalances?: StakingBalances;
totalRewards?: bigint;
totalDelegated?: bigint;
}) {
return (
<div className="flex items-center justify-between">
<AccountStat
title="Total locked"
valueWei={lockedBalances?.locked}
subtitle="Delegated"
subValueWei={0n}
subValueWei={totalDelegated}
/>
<AccountStat
title="Total unlocked"
Expand Down Expand Up @@ -177,12 +188,14 @@ function TableTabs({
addressToGroup,
groupToReward,
groupToIsActivatable,
delegateeToAmount,
activateStake,
}: {
groupToStake?: GroupToStake;
addressToGroup?: AddressTo<ValidatorGroup>;
groupToReward?: AddressTo<number>;
groupToIsActivatable?: AddressTo<boolean>;
delegateeToAmount?: AddressTo<DelegationAmount>;
activateStake: (g: Address) => void;
}) {
const [tab, setTab] = useState<'stakes' | 'rewards' | 'delegations'>('stakes');
Expand Down Expand Up @@ -211,7 +224,7 @@ function TableTabs({
{tab === 'rewards' && (
<RewardsTable groupToReward={groupToReward} addressToGroup={addressToGroup} />
)}
{tab === 'delegations' && <div>TODO</div>}
{tab === 'delegations' && <DelegationsTable delegateeToAmount={delegateeToAmount} />}
</div>
);
}
3 changes: 3 additions & 0 deletions src/config/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
97 changes: 97 additions & 0 deletions src/features/delegation/DelegationsTable.tsx
Original file line number Diff line number Diff line change
@@ -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<DelegationAmount>;
addressToDelegatee?: AddressTo<ValidatorGroup>;
}) {
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 <FullWidthSpinner>Loading delegation data</FullWidthSpinner>;
}

if (!objLength(delegateeToAmount)) {
return (
<HeaderAndSubheader
header="No funds delegated"
subHeader={`You currently have no delegations. You can delegate voting power to contribute to Celo governance.`}
className="my-10"
>
<SolidButton onClick={() => showTxModal()}>Delegate CELO</SolidButton>
</HeaderAndSubheader>
);
}

return (
<div className="mt-4 space-y-2">
<StackedBarChart data={chartData} />
<table className="w-full">
<thead>
<tr>
<th className={tableClasses.th}>Name</th>
<th className={tableClasses.th}>Amount</th>
<th className={tableClasses.th}>%</th>
</tr>
</thead>
<tbody>
{tableData.map(({ address, name, amount, percentage }) => (
<tr key={address}>
<td className={tableClasses.td}>
<ValidatorGroupLogoAndName address={address} name={name} />
</td>
<td className={tableClasses.td}>{formatNumberString(amount, 2) + ' CELO'}</td>
<td className={tableClasses.td}>{percentage + '%'}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
9 changes: 9 additions & 0 deletions src/features/delegation/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface DelegationAmount {
expected: bigint;
real: bigint;
}

export interface DelegationBalances {
percentDelegated: bigint;
delegateeToAmount: AddressTo<DelegationAmount>;
}
88 changes: 88 additions & 0 deletions src/features/delegation/useDelegationBalances.ts
Original file line number Diff line number Diff line change
@@ -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<DelegationBalances> {
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<any> = 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;
}
12 changes: 5 additions & 7 deletions src/features/staking/ActiveStakesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})),
Expand Down Expand Up @@ -91,14 +92,11 @@ export function ActiveStakesTable({
</tr>
</thead>
<tbody>
{tableData.map(({ address, stake, percentage }) => (
{tableData.map(({ address, name, stake, percentage }) => (
<tr key={address}>
<td className={tableClasses.td}>
<div className="flex items-center space-x-5">
<ValidatorGroupLogoAndName
address={address}
name={addressToGroup?.[address]?.name}
/>
<ValidatorGroupLogoAndName address={address} name={name} />
{groupToIsActivatable?.[address] && (
<span className="rounded-full bg-gray-200 px-1.5 py-0.5 text-xs text-gray-400">
Pending
Expand Down
12 changes: 5 additions & 7 deletions src/features/staking/rewards/RewardsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})),
Expand Down Expand Up @@ -75,13 +76,10 @@ export function RewardsTable({
</tr>
</thead>
<tbody>
{tableData.map(({ address, reward, percentage }) => (
{tableData.map(({ address, name, reward, percentage }) => (
<tr key={address}>
<td className={tableClasses.td}>
<ValidatorGroupLogoAndName
address={address}
name={addressToGroup?.[address]?.name}
/>
<ValidatorGroupLogoAndName address={address} name={name} />
</td>
<td className={tableClasses.td}>{formatNumberString(reward, 2) + ' CELO'}</td>
<td className={tableClasses.td}>{percentage + '%'}</td>
Expand Down
2 changes: 1 addition & 1 deletion src/features/staking/useStakingBalances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/features/validators/ValidatorGroupLogo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function ValidatorGroupLogoAndName({
<div className={`flex items-center ${className}`}>
<ValidatorGroupLogo address={address} size={size} />
<div className="ml-2 flex flex-col">
<span>{name || 'Unknown'}</span>
<span>{name || 'Unknown Group'}</span>
<span className="font-mono text-xs text-taupe-600">{shortenAddress(address)}</span>
</div>
</div>
Expand Down

0 comments on commit 8b16de9

Please sign in to comment.