diff --git a/next.config.js b/next.config.js index f9e9bb0..3ab30e0 100644 --- a/next.config.js +++ b/next.config.js @@ -12,6 +12,7 @@ const CONNECT_SRC_HOSTS = [ 'https://*.walletconnect.com', 'wss://*.walletconnect.com', 'wss://*.walletconnect.org', + 'https://api.github.com', 'https://raw.githubusercontent.com', 'https://celo-mainnet.infura.io', ]; @@ -33,7 +34,9 @@ const cspHeader = ` frame-ancestors 'none'; ${!isDev ? 'block-all-mixed-content;' : ''} ${!isDev ? 'upgrade-insecure-requests;' : ''} -`.replace(/\s{2,}/g, ' ').trim(); +` + .replace(/\s{2,}/g, ' ') + .trim(); const securityHeaders = [ { @@ -59,13 +62,13 @@ const securityHeaders = [ value: cspHeader, }, ] - : []) + : []), ]; module.exports = { webpack: (config) => { - config.externals = [...config.externals, 'pino-pretty'] - return config + config.externals = [...config.externals, 'pino-pretty']; + return config; }, async headers() { @@ -74,14 +77,14 @@ module.exports = { source: '/(.*)', headers: securityHeaders, }, - ] + ]; }, images: { remotePatterns: [ { - protocol: "https", - hostname: "**", + protocol: 'https', + hostname: '**', }, ], }, diff --git a/src/app/account/page.tsx b/src/app/account/page.tsx index d2854ab..6c2ff12 100644 --- a/src/app/account/page.tsx +++ b/src/app/account/page.tsx @@ -24,9 +24,9 @@ import { useTransactionModal } from 'src/features/transactions/TransactionModal' import { TxModalType } from 'src/features/transactions/types'; import { ValidatorGroup } from 'src/features/validators/types'; import { useValidatorGroups } from 'src/features/validators/useValidatorGroups'; -import Lock from 'src/images/icons/lock.svg'; -import Unlock from 'src/images/icons/unlock.svg'; -import Withdraw from 'src/images/icons/withdraw.svg'; +import LockIcon from 'src/images/icons/lock.svg'; +import UnlockIcon from 'src/images/icons/unlock.svg'; +import WithdrawIcon from 'src/images/icons/withdraw.svg'; import { usePageInvariant } from 'src/utils/navigation'; import { useAccount } from 'wagmi'; @@ -88,7 +88,7 @@ function LockButtons({ className }: { className?: string }) { onClick={() => showTxModal(TxModalType.Lock, { defaultAction: LockActionType.Lock })} >
- + Lock
@@ -96,7 +96,7 @@ function LockButtons({ className }: { className?: string }) { onClick={() => showTxModal(TxModalType.Lock, { defaultAction: LockActionType.Unlock })} >
- + Unlock
@@ -104,7 +104,7 @@ function LockButtons({ className }: { className?: string }) { onClick={() => showTxModal(TxModalType.Lock, { defaultAction: LockActionType.Withdraw })} >
- + Withdraw
diff --git a/src/app/governance/page.tsx b/src/app/governance/page.tsx index 9280ea6..6fd4930 100644 --- a/src/app/governance/page.tsx +++ b/src/app/governance/page.tsx @@ -4,24 +4,23 @@ import Image from 'next/image'; import { useMemo, useState } from 'react'; import { Fade } from 'src/components/animation/Fade'; import { SpinnerWithLabel } from 'src/components/animation/Spinner'; -import { ExternalLink } from 'src/components/buttons/ExternalLink'; import { TabHeaderFilters } from 'src/components/buttons/TabHeaderButton'; import { SearchField } from 'src/components/input/SearchField'; 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 { useLockedBalance } from 'src/features/account/hooks'; import { ProposalCard } from 'src/features/governance/ProposalCard'; import { ProposalStage } from 'src/features/governance/contractTypes'; +import { GetInvolvedCtaCard, NoFundsLockedCtaCard } from 'src/features/governance/ctaCards'; 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'; -import DiscordIcon from 'src/images/logos/discord.svg'; import { useIsMobile } from 'src/styles/mediaQueries'; +import { isNullish } from 'src/utils/typeof'; +import { useAccount } from 'wagmi'; enum Filter { All = 'All', @@ -38,7 +37,7 @@ export default function Page() {
- +
); @@ -48,6 +47,8 @@ function ProposalList() { const isMobile = useIsMobile(); const { proposals } = useGovernanceProposals(); + const { address } = useAccount(); + const { lockedBalance } = useLockedBalance(address); const [searchQuery, setSearchQuery] = useState(''); const [filter, setFilter] = useState(Filter.All); @@ -66,14 +67,14 @@ function ProposalList() { }, [proposals]); return ( -
+

Proposals

...} - modal={() => } + modal={() => } />
-
+ {address && !isNullish(lockedBalance) && lockedBalance <= 0n && } {filteredProposals ? (
{filteredProposals.length ? ( @@ -116,41 +117,6 @@ function ProposalList() { ); } -function CtaModal() { - return ( -
-

Get Involved

- -
- -
- Explore the docs -
- -
- -
- Join the chat -
- -
- -
- Join the forum -
-
- ); -} - function useFilteredProposals({ proposals, filter, diff --git a/src/config/proposals.json b/src/config/proposals.json index 9655b33..8f6f766 100644 --- a/src/config/proposals.json +++ b/src/config/proposals.json @@ -454,7 +454,7 @@ "timestampExecuted": 1655337600000 }, { - "cgp": 17, + "cgp": 54, "title": "Reopening of the Celo Community Fund", "author": "@Maya-R-B @ThePassiveTrust @aaronmboyd", "stage": 6, @@ -473,7 +473,7 @@ "cgp": 56, "title": "One-to-one stable value asset basket and 50/50 DAI-USDC-Split", "author": "Roman Croessmann (@rcroessmann), Markus Franke (@MarkusBerlin), Slobodan Sudaric (@sudarics)", - "stage": 1, + "stage": 6, "id": 62, "url": "https://forum.celo.org/t/reserve-mandate-1-1-stable-value-asset-basket/3663", "timestamp": 1655164800000 @@ -530,6 +530,14 @@ "url": "https://forum.celo.org/t/core-contracts-release-8/4050", "timestamp": 1659657600000 }, + { + "cgp": 63, + "title": "Learn to Earn", + "author": "Débora Conconi (@deboraconconi)", + "stage": 0, + "url": "https://forum.celo.org/t/proposal-cgp-63-learn-to-earn-campaign-with-creal-and-celo-in-brazil/4772", + "timestamp": 1667692800000 + }, { "cgp": 64, "title": "Celo India DAO (Regional DAO)", @@ -542,18 +550,38 @@ "cgp": 65, "title": "(WITHDRAWN - VOTE NO) Top-Up of Governance-Owned cUSD<->USDC Liquidity", "author": "Roman Croessmann(@rcroessmann)", - "stage": 0, + "stage": 7, + "id": 79, "url": "https://forum.celo.org/t/discussion-cgp-61-on-chain-68-governance-owned-cusd-usdc-liquidity/4036/16", "timestamp": 1669248000000 }, + { + "cgp": 66, + "title": "Increase target level of stable holdings in the reserve", + "author": "Roman Croessman (@rcroessmann), Markus Franke (@MarkusBerlin)", + "stage": 6, + "id": 56, + "url": "https://forum.celo.org/t/upcoming-cgp-increase-target-level-of-stable-holdings-in-the-reserve/3380", + "timestamp": 1652400000000 + }, { "cgp": 67, "title": "Enabling T-Systems as an oracle provider and deploy oracle payments", "author": "Martin Volpe (@martinvol)", - "stage": 1, + "stage": 6, + "id": 77, "url": "https://forum.celo.org/t/decentralized-oracles/3610", "timestamp": 1670198400000 }, + { + "cgp": 68, + "title": "Celo Africa DAO", + "author": "[Khadeeejah](https://github.com/Khadeeejah)", + "stage": 6, + "id": 85, + "url": "https://forum.celo.org/t/celo-africa-regional-dao-proposal/4054/50?u=aliu", + "timestamp": 1670457600000 + }, { "cgp": 69, "title": "Move Mento-Owned cUSD<->USDC Liquidity from Mobius to Curve", @@ -598,9 +626,9 @@ }, { "cgp": 74, - "title": "Mento Upgrade 01: MultiCollateral Support", + "title": "Mento Upgrade 01 - MultiCollateral Support", "author": "Bogdan Dumitru ", - "stage": 1, + "stage": 6, "id": 91, "url": "https://forum.celo.org/t/proposal-mu01-phase1-multicollateral-mento/5245", "timestamp": 1679443200000 @@ -646,7 +674,7 @@ "cgp": 79, "title": "Mento Reserve Returning CELO", "author": "Roman Croessmann (@rcroessmann)", - "stage": 1, + "stage": 6, "id": 102, "url": "https://forum.celo.org/t/mento-reserve-returning-celo/5236/36", "timestamp": 1682035200000 @@ -655,17 +683,17 @@ "cgp": 80, "title": "Follow-on Funding for Climate Collective", "author": "Anna Lerner, Nirvaan Ranganathan, Ed Walters", - "stage": 1, + "stage": 6, "id": 105, "url": "https://forum.celo.org/t/climate-collective-1-year-anniversary-heres-to-many-more/5585", "timestamp": 1682380800000 }, { "cgp": 81, - "title": "Mento Upgrade 01 Patch 1: MultiCollateral Support", + "title": "Mento Upgrade 01 Patch 1 - MultiCollateral Support", "author": "Bayo Sodimu ", "stage": 1, - "id": 109, + "id": 110, "url": "https://forum.celo.org/t/proposal-cgp081-mu01-patch1-multi-collateral-mento-decimal-fix/5787", "timestamp": 1683158400000 }, @@ -691,7 +719,7 @@ "cgp": 84, "title": "Funding for Credit Collective", "author": "Tomer Bariach, Reuven Palatnik", - "stage": 1, + "stage": 6, "id": 117, "url": "https://forum.celo.org/t/credit-collective-hello-world/5789", "timestamp": 1688688000000 @@ -708,7 +736,7 @@ "cgp": 86, "title": "Adding USDC/EUR and USDC/BRL oracles", "author": "Nelson Taveras (@nvtaveras)", - "stage": 1, + "stage": 6, "id": 119, "url": "https://forum.celo.org/t/mento-upgrade-1-deployment-timeline/5219/11", "timestamp": 1689206400000 @@ -717,7 +745,7 @@ "cgp": 87, "title": "Temperature Check - Celo transition to an Ethereum L2", "author": "productmatt", - "stage": 1, + "stage": 6, "id": 116, "url": "https://forum.celo.org/t/clabs-proposal-for-celo-to-transition-to-an-ethereum-l2/6109" }, @@ -725,7 +753,7 @@ "cgp": 88, "title": "CELO Korea", "author": "Hope Lee (hopelee327@gmail.com), Alex Shin (alex@shinlabs.xyz, @82partners)", - "stage": 1, + "stage": 6, "id": 120, "url": "https://forum.celo.org/t/celo-asia-dao-proposal/5957/14", "timestamp": 1690070400000 @@ -742,15 +770,16 @@ "cgp": 90, "title": "CELO/wETH UniswapV3 Pool", "author": "Roman Croessmann ", - "stage": 1, + "stage": 6, + "id": 121, "timestamp": 1690934400000 }, { "cgp": 91, "title": "Road to COP28", - "author": "jeanne bloch , ", - "stage": 1, - "id": 128, + "author": "jeanne bloch ", + "stage": 8, + "id": 122, "url": "https://forum.celo.org/t/proposal-web3-x-sdg-event-proposal-for-road-to-cop28/6225", "timestamp": 1691539200000 }, @@ -758,7 +787,7 @@ "cgp": 92, "title": "Proposal for mobile first community in celo ecosystem and telecom", "author": "@jeffpulver", - "stage": 1, + "stage": 6, "id": 123, "url": "https://forum.celo.org/t/proposal-for-60-000-funding-to-support-a-mobile-first-community-in-celo-ecosystem/6313", "timestamp": 1691452800000 @@ -776,7 +805,7 @@ "cgp": 94, "title": "EUROC Reserve Collateral and additional Oracle Rates", "author": "@nvtaveras, @rcroessmann", - "stage": 1, + "stage": 6, "id": 127, "url": "https://forum.celo.org/t/dunia-launching-exof-on-celo/6261", "timestamp": 1692230400000 @@ -785,7 +814,7 @@ "cgp": 95, "title": "Centrifuge Deployment on Celo", "author": "Asad Khan, Nirvaan Ranganathan", - "stage": 1, + "stage": 6, "id": 130, "url": "https://forum.celo.org/t/centrifuge-deployment-on-celo/6405", "timestamp": 1692662400000 @@ -803,7 +832,7 @@ "cgp": 97, "title": "MU03 Enable more trading pairs and continue phasing out V1", "author": "Bogdan Dumitru , Philip Rätsch ", - "stage": 1, + "stage": 6, "id": 133, "url": "https://forum.celo.org/t/proposal-mento-upgrade-mu03-enable-more-trading-pairs-and-continue-phasing-out-v1/6462", "timestamp": 1693440000000 @@ -812,7 +841,7 @@ "cgp": 98, "title": "Add EUROC/XOF oracles", "author": "@nvtaveras", - "stage": 1, + "stage": 6, "id": 134, "url": "https://forum.celo.org/t/dunia-launching-exof-on-celo/6261", "timestamp": 1694476800000 @@ -821,7 +850,7 @@ "cgp": 99, "title": "Celo Core Contracts Release 10", "author": "Martin Volpe (@martinvol), Pavel Hornak (@pahor167)", - "stage": 1, + "stage": 6, "id": 135, "url": "https://forum.celo.org/t/contracts-release-10-proposal-on-chain/6563", "timestamp": 1694649600000 @@ -830,7 +859,7 @@ "cgp": 100, "title": "Set baseFeeOpCodeActivationBlock on GasPriceMinimum for Gingerbread hardfork", "author": "Martín Volpe (@martinvol), Gastón Ponti (@gastonponti), Pavel Hornak (@pahor167)", - "stage": 1, + "stage": 6, "id": 136, "url": "https://forum.celo.org/t/contract-release-10-proposal-on-chain/6563", "timestamp": 1694736000000 @@ -908,7 +937,7 @@ "cgp": 109, "title": "MU04 - Transition to Mento V2 and update StableToken implementation", "author": "Philip Rätsch ", - "stage": 1, + "stage": 6, "id": 144, "url": "https://forum.celo.org/t/proposal-mento-upgrade-mu04-phase-out-v1-fully-transition-to-v2-and-update-stabletoken/7034", "timestamp": 1701820800000 @@ -917,7 +946,8 @@ "cgp": 110, "title": "Mentors Collective", "author": "Sharon Sciammas ", - "stage": 0, + "stage": 1, + "id": 146, "url": "https://forum.celo.org/t/mentors-collective-hello-world/6884/", "timestamp": 1702252800000 }, @@ -925,7 +955,7 @@ "cgp": 111, "title": "Strategic Grant For Accelerating Celo Ecosystem Adoption Across Africa", "author": "Opera MiniPay (@opera_minipay)", - "stage": 1, + "stage": 6, "id": 147, "url": "https://forum.celo.org/t/strategic-grant-for-accelerating-celo-ecosystem-adoption-across-africa/6969", "timestamp": 1702339200000 @@ -935,7 +965,7 @@ "title": "Celo Tribe University Society follow-up", "author": "Hawwal Ogungbadero @hawwal", "stage": 1, - "id": 154, + "id": 156, "url": "https://forum.celo.org/t/proposal-celo-tribe-web3-social-club-for-universities/4861/22?u=hawwal", "timestamp": 1702425600000 }, @@ -943,7 +973,7 @@ "cgp": 113, "title": "Invest part of Reserve Collateral into low-risk reward generating protocols", "author": "Roman Croessmann (@rcroessmann), Markus Franke (@MarkusBerlin), Oleksiy Novykov", - "stage": 1, + "stage": 6, "id": 150, "url": "https://forum.celo.org/t/yield-rewards-on-reserve-collateral/6881", "timestamp": 1704758400000 @@ -952,7 +982,7 @@ "cgp": 114, "title": "Funding payment contracts for Oracle providers", "author": "Denvil Jamal Clarke (@denviljclarke), Nelson Taveras (@nvtaveras)", - "stage": 1, + "stage": 6, "id": 151, "url": "https://forum.celo.org/t/decentralized-oracles/3610", "timestamp": 1705017600000 @@ -961,8 +991,8 @@ "cgp": 115, "title": "Revised Celo Governance Guidelines and Public Goods Funding Strategy H1 2024", "author": "Luuk Weber(@LuukDAO), Monty Bryant(@MontyMerlin), Roman Croessmann (@rcroessmann), Bogdan Dumitru(@bowd)", - "stage": 1, - "id": 115, + "stage": 6, + "id": 157, "url": "https://forum.celo.org/t/draft-celo-governance-guidelines-and-public-goods-funding-strategy-h1-2024/7200", "timestamp": 1707264000000 }, @@ -970,7 +1000,8 @@ "cgp": 116, "title": "Celo Ecosystem Liquidity Operation Guild (CELO-Guild)", "author": "Henry He", - "stage": 0, + "stage": 5, + "id": 155, "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 } diff --git a/src/features/governance/ProposalCard.tsx b/src/features/governance/ProposalCard.tsx index 0113e34..975040b 100644 --- a/src/features/governance/ProposalCard.tsx +++ b/src/features/governance/ProposalCard.tsx @@ -18,7 +18,7 @@ export function ProposalCard({ data }: { data: MergedProposalData }) { const { id, timestamp, expiryTimestamp, votes } = proposal || {}; const { title, timestamp: cgpTimestamp, timestampExecuted, cgp } = metadata || {}; - const idValue = id ? `# ${id}` : cgp ? `CGP ${cgp}` : undefined; + const idValue = cgp ? `CGP ${cgp}` : id ? `# ${id}` : undefined; const titleValue = title ? trimToLength(title, 50) : undefined; const proposedTimestamp = timestamp || cgpTimestamp; const proposedTimeValue = proposedTimestamp diff --git a/src/features/governance/contractTypes.ts b/src/features/governance/contractTypes.ts index 0ca737d..f224229 100644 --- a/src/features/governance/contractTypes.ts +++ b/src/features/governance/contractTypes.ts @@ -43,6 +43,13 @@ export const ProposalStageToStyle: Record +

Get Involved

+ +
+ +
+ Explore the docs +
+ +
+ +
+ Join the chat +
+ +
+ +
+ Join the forum +
+
+ ); +} + +export function NoFundsLockedCtaCard() { + const showTxModal = useTransactionModal(TxModalType.Lock); + + return ( +
+
+

{`You can’t participate in governance, yet.`}

+

+ Lock CELO to vote on proposals and stake with validators. +

+
+ showTxModal()}> +
+ + Lock +
+
+
+ ); +} diff --git a/src/features/governance/fetchFromRepository.ts b/src/features/governance/fetchFromRepository.ts index 8517153..f8c46e2 100644 --- a/src/features/governance/fetchFromRepository.ts +++ b/src/features/governance/fetchFromRepository.ts @@ -8,15 +8,18 @@ import { logger } from 'src/utils/logger'; import { objLength } from 'src/utils/objects'; import { parse as parseYaml } from 'yaml'; +// TODO use official repo when fixes are merged // const GITHUB_OWNER = 'celo-org'; const GITHUB_OWNER = 'jmrossy'; const GITHUB_REPO = 'governance'; const GITHUB_DIRECTORY_PATH = 'CGPs'; // const GITHUB_BRANCH = 'main'; -const GITHUB_BRANCH = 'metadata-fixes'; -const GITHUB_NAME_REGEX = /^cgp-\d+\.md$/; +const GITHUB_BRANCH = 'missing-proposal-ids'; +const GITHUB_NAME_REGEX = /^cgp-(\d+)\.md$/; -export async function fetchProposalsFromRepo(): Promise { +export async function fetchProposalsFromRepo( + cache: ProposalMetadata[] = [], +): Promise { const files = await fetchGithubDirectory( GITHUB_OWNER, GITHUB_REPO, @@ -26,8 +29,22 @@ export async function fetchProposalsFromRepo(): Promise { ); const errorUrls = []; const validProposals: ProposalMetadata[] = []; - for (let i = 0; i < files.length; i++) { - const file = files[i]; + for (const file of files) { + // First extract cgp number and check cache + const cgpString = GITHUB_NAME_REGEX.exec(file.name)?.[1]; + if (!cgpString) { + logger.error('Failed to extract CGP number from file name', file.name); + errorUrls.push(file.download_url); + continue; + } + const cgpNumber = parseInt(cgpString, 10); + const cachedProposal = cache.find((p) => p.cgp === cgpNumber); + if (cachedProposal) { + validProposals.push(cachedProposal); + continue; + } + + // If it's not in the cache, fetch the file and parse it const content = await fetchGithubFile(file); if (!content) { errorUrls.push(file.download_url); diff --git a/src/features/governance/repoTypes.ts b/src/features/governance/repoTypes.ts index 162abaf..31362f8 100644 --- a/src/features/governance/repoTypes.ts +++ b/src/features/governance/repoTypes.ts @@ -12,6 +12,8 @@ export enum ProposalMetadataStatus { export const MetadataStatusToStage: Record = { [ProposalMetadataStatus.DRAFT]: ProposalStage.None, + // Note, some proposals are listed as PROPOSED but are actually completed/expired + // There's some logic in the proposal merging to help account for this [ProposalMetadataStatus.PROPOSED]: ProposalStage.Queued, [ProposalMetadataStatus.EXECUTED]: ProposalStage.Executed, [ProposalMetadataStatus.EXPIRED]: ProposalStage.Expiration, diff --git a/src/features/governance/useGovernanceProposals.ts b/src/features/governance/useGovernanceProposals.ts index 11e8976..c1cdbac 100644 --- a/src/features/governance/useGovernanceProposals.ts +++ b/src/features/governance/useGovernanceProposals.ts @@ -1,14 +1,20 @@ import { governanceABI } from '@celo/abis'; import { useQuery } from '@tanstack/react-query'; import { useToastError } from 'src/components/notifications/useToastError'; +import { + APPROVAL_STAGE_EXPIRY_TIME, + EXECUTION_STAGE_EXPIRY_TIME, + QUEUED_STAGE_EXPIRY_TIME, +} from 'src/config/consts'; import { Addresses } from 'src/config/contracts'; import CachedMetadata from 'src/config/proposals.json'; import { - FAILED_PROPOSAL_STAGES, + ACTIVE_PROPOSAL_STAGES, Proposal, ProposalStage, VoteValue, } from 'src/features/governance/contractTypes'; +import { fetchProposalsFromRepo } from 'src/features/governance/fetchFromRepository'; import { ProposalMetadata } from 'src/features/governance/repoTypes'; import { logger } from 'src/utils/logger'; import { MulticallReturnType, PublicClient } from 'viem'; @@ -29,8 +35,9 @@ export function useGovernanceProposals() { logger.debug('Fetching governance proposals'); // Fetch on-chain data const proposals = await fetchGovernanceProposals(publicClient); + const metadata = await fetchGovernanceMetadata(); // Then merge it with the cached - return mergeProposalsWithMetadata(proposals); + return mergeProposalsWithMetadata(proposals, metadata); }, gcTime: Infinity, staleTime: 60 * 60 * 1000, // 1 hour @@ -76,7 +83,7 @@ async function fetchGovernanceProposals(publicClient: PublicClient): Promise ({ id, upvotes: queuedUpvotes[i] })), ...dequeuedIds.map((id) => ({ id, upvotes: 0n })), - ]; + ].filter((p) => p.id !== 0n); if (!allIdsAndUpvotes.length) return []; @@ -122,13 +129,16 @@ async function fetchGovernanceProposals(publicClient: PublicClient): Promise { - const sortedMetadata = [...CachedMetadata].sort((a, b) => b.cgp - a.cgp) as ProposalMetadata[]; +function fetchGovernanceMetadata(): Promise { + // Fetching every past proposal would take too long so the app + // fetches them at build time and stores a cache. To keep this + // hook fast, the app should be re-built every now and then. + const cached = CachedMetadata as ProposalMetadata[]; + return fetchProposalsFromRepo(cached); +} + +function mergeProposalsWithMetadata( + proposals: Proposal[], + metadata: ProposalMetadata[], +): Array { const sortedProposals = [...proposals].sort((a, b) => b.id - a.id); + const sortedMetadata = [...metadata].sort((a, b) => b.cgp - a.cgp); const merged: Array = []; for (const proposal of sortedProposals) { const metadataIndex = sortedMetadata.findIndex((m) => m.id === proposal.id); if (metadataIndex >= 0) { + // Remove the metadata element and use the on-chain stage const metadata = sortedMetadata.splice(metadataIndex, 1)[0]; merged.push({ stage: proposal.stage, proposal, metadata }); } else { + // No metadata found, use just the on-chain data merged.push({ stage: proposal.stage, proposal }); } } - // Merge in any remaining metadata + // Merge in any remaining metadata, cleaning it first for (const metadata of sortedMetadata) { + if (metadata.stage === ProposalStage.Queued) { + if (metadata.id) { + // Any proposals marked 'PROPOSED' in the metadata but not found on-chain + // Are either executed or expired. It's uncertain so they're marked as expired + metadata.stage = ProposalStage.Expiration; + } else { + // If there's no ID, it's a draft + metadata.stage = ProposalStage.None; + } + } 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; - }); + // Filter out failed proposals without a corresponding CGP + return ( + merged + .filter((p) => p.metadata?.cgp || p.proposal?.stage !== ProposalStage.Expiration) + // Sort by active proposals then by CGP number + .sort((a, b) => { + if (isActive(b) && !isActive(a)) return 1; + if (isActive(a) && !isActive(b)) return -1; + if (a.metadata && b.metadata) return b.metadata.cgp - a.metadata.cgp; + if (b.metadata) return 1; + if (a.metadata) return -1; + return 0; + }) + ); +} + +function isActive(p: MergedProposalData) { + return ACTIVE_PROPOSAL_STAGES.includes(p.stage); } -function isFailed(p?: Proposal | ProposalMetadata) { - return p && FAILED_PROPOSAL_STAGES.includes(p.stage); +function getExpiryTimestamp(stage: ProposalStage, timestamp: number) { + if (stage === ProposalStage.Queued) { + return timestamp + QUEUED_STAGE_EXPIRY_TIME; + } else if (stage === ProposalStage.Approval) { + return timestamp + APPROVAL_STAGE_EXPIRY_TIME; + } else if (stage === ProposalStage.Execution) { + return timestamp + EXECUTION_STAGE_EXPIRY_TIME; + } else { + return undefined; + } }