Skip to content

Commit

Permalink
Implement governance data merging
Browse files Browse the repository at this point in the history
  • Loading branch information
jmrossy committed Feb 16, 2024
1 parent 8d9774d commit ff40550
Show file tree
Hide file tree
Showing 7 changed files with 90 additions and 25 deletions.
3 changes: 2 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
public
src/vendor
src/vendor
src/config/proposals.json
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ For support, you can [file an issue](https://github.com/jmrossy/celo-station/iss

## Development

- Install: `yarn`
- Run locally: `yarn dev`
- Build: `yarn build`
1. Install: `yarn`
2. Setup: `yarn prepare`
3. Run locally: `yarn dev`
41 changes: 27 additions & 14 deletions src/app/governance/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ import { Section } from 'src/components/layout/Section';
import { DropdownModal } from 'src/components/menus/Dropdown';
import { H1 } from 'src/components/text/headers';
import { links } from 'src/config/links';
import { Proposal, ProposalStage } from 'src/features/governance/contractTypes';
import { useGovernanceProposals } from 'src/features/governance/useGovernanceProposals';
import { ProposalStage } from 'src/features/governance/contractTypes';
import {
MergedProposalData,
useGovernanceProposals,
} from 'src/features/governance/useGovernanceProposals';
import BookIcon from 'src/images/icons/book.svg';
import EllipsisIcon from 'src/images/icons/ellipsis.svg';
import CeloIcon from 'src/images/logos/celo.svg';
Expand Down Expand Up @@ -56,8 +59,7 @@ function ProposalList() {
[Filter.Upvoting]: _proposals.filter((p) => p.stage === ProposalStage.Queued).length,
[Filter.Voting]: _proposals.filter((p) => p.stage === ProposalStage.Referendum).length,
[Filter.Drafts]: _proposals.filter((p) => p.stage === ProposalStage.None).length,
//TODO
[Filter.History]: _proposals.filter((p) => p.id === 0).length,
[Filter.History]: _proposals.filter((p) => p.stage > 4).length,
};
}, [proposals]);

Expand Down Expand Up @@ -90,7 +92,7 @@ function ProposalList() {
className="pb-2 all:space-x-4 md:space-x-6"
/>
{filteredProposals.length ? (
filteredProposals.map((proposal) => <Proposal key={proposal.id} proposal={proposal} />)
filteredProposals.map((data, i) => <ProposalCard key={i} data={data} />)
) : (
<div className="flex justify-center py-10">
<p className="text-center text-taupe-600">No proposals found</p>
Expand All @@ -106,12 +108,17 @@ function ProposalList() {
);
}

function Proposal({ proposal }: { proposal: Proposal }) {
function ProposalCard({ data }: { data: MergedProposalData }) {
const { stage, proposal, metadata } = data;

const title =
metadata?.title || metadata?.cgp ? `Proposal CGP-${metadata.cgp}` : `Proposal #${proposal?.id}`;

return (
<div className="flex justify-between">
<div>
<h3 className="font-serif text-lg">{'TODO'}</h3>
<p className="text-gray-600">{proposal.id}</p>
<h3 className="font-serif text-lg">{title}</h3>
<div className="text-sm">{stage}</div>
</div>
</div>
);
Expand Down Expand Up @@ -157,23 +164,29 @@ function useFilteredProposals({
filter,
searchQuery,
}: {
proposals?: Proposal[];
proposals?: MergedProposalData[];
filter: Filter;
searchQuery: string;
}) {
return useMemo<Proposal[] | undefined>(() => {
return useMemo<MergedProposalData[] | undefined>(() => {
if (!proposals) return undefined;
const query = searchQuery.trim().toLowerCase();
return proposals
.filter((p) => {
if (filter === Filter.Upvoting) return p.stage === ProposalStage.Queued;
if (filter === Filter.Voting) return p.stage === ProposalStage.Referendum;
if (filter === Filter.Drafts) return p.stage === ProposalStage.None;
if (filter === Filter.History)
return p.stage === ProposalStage.Expiration || p.stage === ProposalStage.Execution;
if (filter === Filter.History) return p.stage > 4;
return true;
})
.filter((p) => !query || p.url.includes(query))
.sort((a, b) => b.timestamp - a.timestamp);
.filter(
(p) =>
!query ||
p.proposal?.proposer.includes(query) ||
p.proposal?.url.includes(query) ||
p.metadata?.title.includes(query) ||
p.metadata?.author.includes(query) ||
p.metadata?.url?.includes(query),
);
}, [proposals, filter, searchQuery]);
}
2 changes: 1 addition & 1 deletion src/config/proposals.json
Original file line number Diff line number Diff line change
Expand Up @@ -974,4 +974,4 @@
"url": "https://forum.celo.org/t/celo-ecosystem-liquidity-operation-guild-celo-guild-allocate-portion-of-celo-in-the-celo-community-fund-to-solve-liquidity-challenges-faced-by-celo-projects-and-accelerate-the-growth-of-the-celo-ecosystem/7243",
"timestamp": 1706745600000
}
]
]
6 changes: 6 additions & 0 deletions src/features/governance/contractTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ export enum ProposalStage {
Rejected = 8,
}

export const FAILED_PROPOSAL_STAGES = [
ProposalStage.Expiration,
ProposalStage.Rejected,
ProposalStage.Withdrawn,
];

export interface Proposal {
id: number;
timestamp: number;
Expand Down
4 changes: 1 addition & 3 deletions src/features/governance/repoTypes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Proposal, ProposalStage } from 'src/features/governance/contractTypes';
import { ProposalStage } from 'src/features/governance/contractTypes';
import { z } from 'zod';

export enum ProposalMetadataStatus {
Expand Down Expand Up @@ -50,5 +50,3 @@ export interface ProposalMetadata {
author: string;
timestampExecuted?: number;
}

export type ProposalWithMetadata = Proposal & ProposalMetadata;
53 changes: 50 additions & 3 deletions src/features/governance/useGovernanceProposals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,35 @@ import { governanceABI } from '@celo/abis';
import { useQuery } from '@tanstack/react-query';
import { useToastError } from 'src/components/notifications/useToastError';
import { Addresses } from 'src/config/contracts';
import { Proposal, ProposalStage, VoteValue } from 'src/features/governance/contractTypes';
import CachedMetadata from 'src/config/proposals.json';
import {
FAILED_PROPOSAL_STAGES,
Proposal,
ProposalStage,
VoteValue,
} from 'src/features/governance/contractTypes';
import { ProposalMetadata } from 'src/features/governance/repoTypes';
import { logger } from 'src/utils/logger';
import { MulticallReturnType, PublicClient } from 'viem';
import { usePublicClient } from 'wagmi';

export type MergedProposalData = { stage: ProposalStage } & (
| { proposal: Proposal; metadata?: ProposalMetadata }
| { proposal?: Proposal; metadata: ProposalMetadata }
);

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

const { isLoading, isError, error, data } = useQuery({
queryKey: ['useGovernanceProposals', publicClient],
queryFn: () => {
queryFn: async () => {
if (!publicClient) return null;
logger.debug('Fetching governance proposals');
return fetchGovernanceProposals(publicClient);
// Fetch on-chain data
const proposals = await fetchGovernanceProposals(publicClient);
// Then merge it with the cached
return mergeProposalsWithMetadata(proposals);
},
gcTime: Infinity,
staleTime: 60 * 60 * 1000, // 1 hour
Expand Down Expand Up @@ -129,3 +144,35 @@ async function fetchGovernanceProposals(publicClient: PublicClient): Promise<Pro

return proposals;
}

function mergeProposalsWithMetadata(proposals: Proposal[]): Array<MergedProposalData> {
const sortedMetadata = [...CachedMetadata].sort((a, b) => b.cgp - a.cgp) as ProposalMetadata[];
const sortedProposals = [...proposals].sort((a, b) => b.id - a.id);
const merged: Array<MergedProposalData> = [];

for (const proposal of sortedProposals) {
const metadataIndex = sortedMetadata.findIndex((m) => m.id === proposal.id);
if (metadataIndex >= 0) {
const metadata = sortedMetadata.splice(metadataIndex, 1)[0];
merged.push({ stage: proposal.stage, proposal, metadata });
} else {
merged.push({ stage: proposal.stage, proposal });
}
}

// Merge in any remaining metadata
for (const metadata of sortedMetadata) {
merged.push({ stage: metadata.stage, metadata });
}

// Push failed proposals without metadata to the back
return merged.sort((a, b) => {
if (b.metadata && !a.metadata && isFailed(a.proposal)) return 1;
else if (a.metadata && !b.metadata && isFailed(b.proposal)) return -1;
return 0;
});
}

function isFailed(p?: Proposal | ProposalMetadata) {
return p && FAILED_PROPOSAL_STAGES.includes(p.stage);
}

0 comments on commit ff40550

Please sign in to comment.