Next.js application for Celo governance (proposals, voting, staking, delegation). Uses PostgreSQL (Drizzle ORM), viem for on-chain reads, and React Query for data fetching.
Proposals follow this lifecycle: Queued(1) → Referendum(3) → Execution(4) → Executed(6)/Expiration(5)
Off-chain stages (not in Solidity): Executed(6), Withdrawn(7), Rejected(8), Adopted(9)
Stage updates happen via two mechanisms:
- Event-based (backfill):
ProposalQueued,ProposalExecuted, etc. events update the DB - Cron-based (every 5 min):
src/scripts/updateProposalStages.tspollsgetProposalStage()on-chain for active proposals — needed because Referendum→Execution emits no event
Client-side fallback: getEffectiveStage() in governanceData.ts optimistically advances the displayed stage when the calculated end time has passed but the DB hasn't caught up yet.
PRIVATE_NO_RATE_LIMITED_NODE: Archive node for historical reads (quorum calculation, event lookups). Can become stale.- Forno (
forno.celo.org): Public RPC, rate-limited, no archive state. Used as fallback for current-state reads. - The cron job checks archive node freshness against forno and falls back for
getProposalStage()calls when stale.
- Check on-chain stage:
cast call 0xD533Ca259b330c7A88f74E000a3FaEa2d63B7972 "getProposalStage(uint256)(uint256)" <PROPOSAL_ID> --rpc-url https://forno.celo.org - Check DB stage:
SELECT id, stage FROM proposals WHERE id = <PROPOSAL_ID>; - If they differ, check the archive node:
cast block latest --rpc-url "https://rpc.ankr.com/celo" -f numbervscast block latest --rpc-url https://forno.celo.org -f number— if the private node's block is significantly lower, it's stale - Check cron logs:
gh run list --workflow="update-proposal-stages.yml" --limit 5thengh run view <RUN_ID> --log | grep "Proposal <ID>" - Stage durations (hardcoded in
src/config/consts.ts, must match on-chain): Queued=28d, Referendum=7d, Execution=3d. Verify with:cast call 0xD533Ca259b330c7A88f74E000a3FaEa2d63B7972 "getReferendumStageDuration()(uint256)" --rpc-url https://forno.celo.org
| File | Purpose |
|---|---|
src/scripts/updateProposalStages.ts |
Cron job: polls on-chain stages, updates DB |
src/features/governance/governanceData.ts |
getEffectiveStage(), getStageEndTimestamp() |
src/features/governance/hooks/useGovernanceProposals.ts |
Frontend data assembly, applies effective stage |
src/features/governance/components/ProposalTimeline.tsx |
Timeline UI component |
src/features/governance/components/StageBadge.tsx |
Stage badge rendering |
src/features/governance/types.ts |
ProposalStage enum |
src/config/consts.ts |
Stage duration constants |
.github/workflows/update-proposal-stages.yml |
Cron workflow (every 5 min) |
POSTGRES_URL="postgresql://postgres:postgres@localhost:5432/postgres" npx tsx src/scripts/updateProposalStages.tsnpx vitest run src/scripts/updateProposalStages.test.ts
npx vitest run src/features/governance/governanceData.test.ts
npx vitest run src/features/governance/components/ProposalTimeline.test.ts