Skip to content

Commit

Permalink
Start impl of validator fetching
Browse files Browse the repository at this point in the history
  • Loading branch information
jmrossy committed Dec 17, 2023
1 parent 4738029 commit 491ef1c
Show file tree
Hide file tree
Showing 12 changed files with 462 additions and 12 deletions.
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@tanstack/eslint-plugin-query/recommended",
"next",
"next/core-web-vitals",
"prettier"
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"wagmi": "1.4.12"
},
"devDependencies": {
"@tanstack/eslint-plugin-query": "^5.12.1",
"@types/jest": "^29.5.11",
"@types/node": "^20.10.4",
"@types/react": "^18.2.45",
Expand Down
8 changes: 4 additions & 4 deletions src/app/app.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Analytics } from '@vercel/analytics/react';
import { PropsWithChildren } from 'react';
import { PropsWithChildren, useState } from 'react';
import { ToastContainer, Zoom, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { ErrorBoundary } from 'src/components/errors/ErrorBoundary';
Expand All @@ -11,8 +11,6 @@ import { WagmiContext } from 'src/config/wagmi';
import { useIsSsr } from 'src/utils/ssr';
import 'src/vendor/inpage-metamask';

const reactQueryClient = new QueryClient({});

function SafeHydrate({ children }: PropsWithChildren<any>) {
// Disable app SSR for now as it's not needed and
// complicates wallet integrations
Expand All @@ -25,10 +23,12 @@ function SafeHydrate({ children }: PropsWithChildren<any>) {
}

export function App({ children }: PropsWithChildren<any>) {
const [queryClient] = useState(() => new QueryClient());

return (
<ErrorBoundary>
<SafeHydrate>
<QueryClientProvider client={reactQueryClient}>
<QueryClientProvider client={queryClient}>
<WagmiContext>
<BodyLayout>{children}</BodyLayout>
<ToastContainer transition={Zoom} position={toast.POSITION.BOTTOM_RIGHT} />
Expand Down
1 change: 1 addition & 0 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
'use client';
export default function Index() {
return <div className="">TODO</div>;
}
9 changes: 8 additions & 1 deletion src/app/staking/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
'use client';
import { SolidButton } from 'src/components/buttons/SolidButton';
import { Card } from 'src/components/layout/Card';
import { Section } from 'src/components/layout/Section';
import { useValidatorGroups } from 'src/features/validators/hooks';

export default function Index() {
return (
Expand Down Expand Up @@ -41,9 +43,14 @@ function HeroStat({ label, value }: { label: string; value: string }) {
}

function ListSection() {
const { groups } = useValidatorGroups();
return (
<Section>
<Card>TODO</Card>
<Card>
<div className="space-y-4">
{groups?.map((g) => <div key={g.address}>{JSON.stringify(g)}</div>)}
</div>
</Card>
</Section>
);
}
1 change: 1 addition & 0 deletions src/config/consts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
4 changes: 3 additions & 1 deletion src/config/contracts.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Address } from 'viem';

export enum CeloContract {
Accounts = 'Accounts',
DoubleSigningSlasher = 'DoubleSigningSlasher',
Expand All @@ -20,7 +22,7 @@ export const Addresses: Record<CeloContract, Address> = {
[CeloContract.Accounts]: '0x7d21685C17607338b313a7174bAb6620baD0aaB7',
[CeloContract.DoubleSigningSlasher]: '0x50C100baCDe7E2b546371EB0Be1eACcf0A6772ec',
[CeloContract.DowntimeSlasher]: '0x71CAc3B31c138F3327C6cA14f9a1c8d752463fDd',
[CeloContract.Election]: '0x7d21685C17607338b313a7174bAb6620baD0aaB7',
[CeloContract.Election]: '0x8D6677192144292870907E3Fa8A5527fE55A7ff6',
[CeloContract.EpochRewards]: '0x07F007d389883622Ef8D4d347b3f78007f28d8b7',
[CeloContract.GasPriceMinimum]: '0xDfca3a8d7699D8bAfe656823AD60C17cb8270ECC',
[CeloContract.GoldToken]: '0x471EcE3750Da237f93B8E339c536989b8978a438',
Expand Down
1 change: 0 additions & 1 deletion src/features/staking/hooks.ts

This file was deleted.

256 changes: 256 additions & 0 deletions src/features/validators/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
import { accountsABI, electionABI, validatorsABI } from '@celo/abis';
import { useQuery } from '@tanstack/react-query';
import { Addresses } from 'src/config/contracts';
// import { getContract } from 'viem';
import { useEffect } from 'react';
import { toast } from 'react-toastify';
import { ZERO_ADDRESS } from 'src/config/consts';
import { logger } from 'src/utils/logger';
import { PublicClient, usePublicClient } from 'wagmi';
import { Validator, ValidatorGroup, ValidatorStatus } from './types';

export function useValidatorGroups() {
const publicClient = usePublicClient();

const { isLoading, isError, error, data } = useQuery({
queryKey: ['useValidatorGroups', publicClient],
queryFn: async () => {
logger.debug('Fetching validator groups');
const groups = await fetchValidatorGroupInfo(publicClient);
return groups;
},
gcTime: Infinity,
staleTime: Infinity,
});

useEffect(() => {
if (error) {
logger.error(error);
toast.error('Error fetching validator groups');
}
}, [error]);

return {
isLoading,
isError,
groups: data,
};
}

async function fetchValidatorGroupInfo(publicClient: PublicClient) {
// Get contracts
// const cAccounts = getContract({
// address: Addresses.Accounts,
// abi: accountsABI,
// publicClient,
// });
// const cValidators = getContract({
// address: Addresses.Validators,
// abi: validatorsABI,
// publicClient,
// });
// const cElections = getContract({
// address: Addresses.Election,
// abi: electionABI,
// publicClient,
// });

// Fetch list of validators and list of elected signers
// const validatorAddrsP = cValidators.read.getRegisteredValidators()
// const electedSignersP = cElections.read.getCurrentValidatorSigners()
// const [validatorAddrs, electedSigners] = await Promise.all([validatorAddrsP, electedSignersP])
const [validatorAddrsResp, electedSignersResp] = await publicClient.multicall({
contracts: [
{
address: Addresses.Validators,
abi: validatorsABI,
functionName: 'getRegisteredValidators',
},
{
address: Addresses.Election,
abi: electionABI,
functionName: 'getCurrentValidatorSigners',
},
],
});
if (validatorAddrsResp.status !== 'success' || !validatorAddrsResp.result?.length) {
throw new Error('No registered validators found');
}
if (electedSignersResp.status !== 'success' || !electedSignersResp.result?.length) {
throw new Error('No elected signers found');
}
const validatorAddrs = validatorAddrsResp.result;
const electedSignersSet = new Set(electedSignersResp.result);
logger.debug(
`Found ${validatorAddrs.length} validators and ${electedSignersSet.size} elected signers`,
);

// Fetch validator details, needed for their scores and signers
const validatorDetailsRaw = await publicClient.multicall({
contracts: validatorAddrs.map((addr) => ({
address: Addresses.Validators,
abi: validatorsABI,
functionName: 'getValidator',
args: [addr],
})),
});

// https://viem.sh/docs/faq.html#why-is-a-contract-function-return-type-returning-an-array-instead-of-an-object
const validatorDetails = validatorDetailsRaw.map((d, i) => {
if (!d.result) throw new Error(`Validator details missing for index ${i}`);
return {
ecdsaPublicKey: d.result[0],
blsPublicKey: d.result[1],
affiliation: d.result[2],
score: d.result[3],
signer: d.result[4],
};
});
console.log(validatorDetails);

const validatorNames = await publicClient.multicall({
contracts: validatorAddrs.map((addr) => ({
address: Addresses.Accounts,
abi: accountsABI,
functionName: 'getName',
args: [addr],
})),
allowFailure: true,
});

if (
validatorAddrs.length !== validatorDetails.length ||
validatorAddrs.length !== validatorNames.length
) {
throw new Error('Validator list / details size mismatch');
}

// Process validator lists to create list of validator groups
const groups: Record<Address, ValidatorGroup> = {};
for (let i = 0; i < validatorAddrs.length; i++) {
const valAddr = validatorAddrs[i];
const valDetails = validatorDetails[i];
const valName = validatorNames[i].result || '';
const groupAddr = valDetails.affiliation;
// Create new group if there isn't one yet
if (!groups[groupAddr]) {
groups[groupAddr] = {
address: groupAddr,
name: '',
url: '',
members: {},
eligible: false,
capacity: '0',
votes: '0',
};
}
// Create new validator group member
const validatorStatus = electedSignersSet.has(valDetails.signer)
? ValidatorStatus.Elected
: ValidatorStatus.NotElected;
const validator: Validator = {
address: valAddr,
name: valName,
score: valDetails.score.toString(),
signer: valDetails.signer,
status: validatorStatus,
};
groups[groupAddr].members[valAddr] = validator;
}

// // Remove 'null' group with unaffiliated validators
if (groups[ZERO_ADDRESS]) {
delete groups[ZERO_ADDRESS];
}

// Fetch details about the validator groups
const groupAddrs = Object.keys(groups) as Address[];
const groupNames = await publicClient.multicall({
contracts: groupAddrs.map((addr) => ({
address: Addresses.Accounts,
abi: accountsABI,
functionName: 'getName',
args: [addr],
})),
allowFailure: true,
});

// Process details about the validator groups
for (let i = 0; i < groupAddrs.length; i++) {
const groupAddr = groupAddrs[i];
groups[groupAddr].name = groupNames[i].result || groupAddr.substring(0, 10) + '...';
}

// // Fetch vote-related details about the validator groups
// const { eligibleGroups, groupVotes, totalLocked } = await fetchVotesAndTotalLocked()

// // Process vote-related details about the validator groups
// for (let i = 0; i < eligibleGroups.length; i++) {
// const groupAddr = eligibleGroups[i]
// const numVotes = groupVotes[i]
// const group = groups[groupAddr]
// group.eligible = true
// group.capacity = getValidatorGroupCapacity(group, validatorAddrs.length, totalLocked)
// group.votes = numVotes.toString()
// }

return Object.values(groups);
}

// Just fetch latest vote counts, not the entire groups + validators info set
// async function fetchValidatorGroupVotes(groups: ValidatorGroup[]) {
// let totalValidators = groups.reduce((total, g) => total + Object.keys(g.members).length, 0)
// // Only bother to fetch actual num validators on the off chance there are fewer members than MAX
// if (totalValidators < MAX_NUM_ELECTABLE_VALIDATORS) {
// const validators = getContract(CeloContract.Validators)
// const validatorAddrs: string[] = await validators.getRegisteredValidators()
// totalValidators = validatorAddrs.length
// }

// // Fetch vote-related details about the validator groups
// const { eligibleGroups, groupVotes, totalLocked } = await fetchVotesAndTotalLocked()

// // Create map from list provided
// const groupsMap: Record<string, ValidatorGroup> = {}
// for (const group of groups) {
// groupsMap[group.address] = { ...group }
// }

// // Process vote-related details about the validator groups
// for (let i = 0; i < eligibleGroups.length; i++) {
// const groupAddr = eligibleGroups[i]
// const numVotes = groupVotes[i]
// const group = groupsMap[groupAddr]
// if (!group) {
// logger.warn('No group found matching votes, group list must be stale')
// continue
// }
// group.eligible = true
// group.capacity = getValidatorGroupCapacity(group, totalValidators, totalLocked)
// group.votes = numVotes.toString()
// }
// return Object.values(groupsMap)
// }

// async function fetchVotesAndTotalLocked() {
// const lockedGold = getContract(CeloContract.LockedGold)
// const election = getContract(CeloContract.Election)
// const votesP: Promise<EligibleGroupsVotesRaw> = election.getTotalVotesForEligibleValidatorGroups()
// const totalLockedP: Promise<BigNumberish> = lockedGold.getTotalLockedGold()
// const [votes, totalLocked] = await Promise.all([votesP, totalLockedP])
// const eligibleGroups = votes[0]
// const groupVotes = votes[1]
// return { eligibleGroups, groupVotes, totalLocked }
// }

// function getValidatorGroupCapacity(
// group: ValidatorGroup,
// totalValidators: number,
// totalLocked: BigNumberish
// ) {
// const numMembers = Object.keys(group.members).length
// return BigNumber.from(totalLocked)
// .mul(numMembers + 1)
// .div(Math.min(MAX_NUM_ELECTABLE_VALIDATORS, totalValidators))
// .toString()
// }
Loading

0 comments on commit 491ef1c

Please sign in to comment.