diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 8f53630454..80b6b4df6c 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -488,3 +488,4 @@ export function isLitUnavailable(chainId: number) { } export * from "./chains"; +export * from "./programWhitelist"; \ No newline at end of file diff --git a/packages/common/src/programWhitelist.ts b/packages/common/src/programWhitelist.ts new file mode 100644 index 0000000000..8f8909dc43 --- /dev/null +++ b/packages/common/src/programWhitelist.ts @@ -0,0 +1,64 @@ +export type WhitelistStatus = "Accepted" | "Rejected" | "Pending"; + +interface ProgramData { + programId: string; + whitelistStatus: WhitelistStatus; +} + +async function fetchProgramsData(): Promise { + try { + const response = await fetch( + "https://docs.google.com/spreadsheets/d/e/2PACX-1vQxC34V_N3ubt3ycs7LvMya_zYeBmAqTxPczt0yDbLSfpI-kMp6o5E08fC0BxQG4uMp7EPV5bxP-64a/pub?gid=0&single=true&output=csv" + ); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const csvText = await response.text(); + + const stringArray = csvText + .split(/\r?\n/) + .filter((line) => line.trim() !== ""); + + const programsData = stringArray.map((line) => { + const [programId, whitelistStatus] = line.split(",") as [ + string, + WhitelistStatus, + ]; + return { programId, whitelistStatus }; + }); + + return programsData; + } catch (error) { + console.error("Failed to fetch or process the CSV:", error); + return []; + } +} + +export async function getWhitelistedPrograms(): Promise { + const programsData = await fetchProgramsData(); + return programsData + .filter((program) => program.whitelistStatus === "Accepted") + .map((program) => program.programId); +} + +export async function isProgramWhitelisted( + programId: string +): Promise { + const whitelistedPrograms = await getWhitelistedPrograms(); + return whitelistedPrograms.includes(programId); +} + +export async function getAllProgramsData(): Promise { + return await fetchProgramsData(); +} + +export async function getProgramWhitelistStatus( + programId: string +): Promise { + const programsData = await fetchProgramsData(); + const program = programsData.find( + (program) => program.programId === programId + ); + return program ? program.whitelistStatus : null; +} diff --git a/packages/data-layer/src/data-layer.ts b/packages/data-layer/src/data-layer.ts index 9755c4a0fd..88c244036b 100644 --- a/packages/data-layer/src/data-layer.ts +++ b/packages/data-layer/src/data-layer.ts @@ -942,18 +942,25 @@ export class DataLayer { first, orderBy, filter, + whitelistedPrograms, }: { chainIds: number[]; first: number; orderBy?: OrderByRounds; orderDirection?: "asc" | "desc"; filter?: RoundsQueryVariables["filter"]; + whitelistedPrograms?: string[]; }): Promise<{ rounds: RoundGetRound[] }> { return await request(this.gsIndexerEndpoint, getRoundsQuery, { orderBy: orderBy ?? "NATURAL", chainIds, first, - filter, + filter: whitelistedPrograms + ? { + ...filter, + projectId: { in: whitelistedPrograms }, + } + : filter, }); } @@ -1011,8 +1018,8 @@ export class DataLayer { async getAttestationCount({ attestationChainIds, }: { - attestationChainIds: number[] + attestationChainIds: number[]; }): Promise { - return this.attestationService.getAttestationCount({attestationChainIds}); + return this.attestationService.getAttestationCount({ attestationChainIds }); } } diff --git a/packages/grant-explorer/src/features/api/rounds.ts b/packages/grant-explorer/src/features/api/rounds.ts index e18014a473..27dc235086 100644 --- a/packages/grant-explorer/src/features/api/rounds.ts +++ b/packages/grant-explorer/src/features/api/rounds.ts @@ -1,24 +1,31 @@ +import { getWhitelistedPrograms } from "common"; import useSWR, { SWRResponse } from "swr"; import { createISOTimestamp } from "../discovery/utils/createRoundsStatusFilter"; import { RoundGetRound, RoundsQueryVariables, useDataLayer } from "data-layer"; export const useRounds = ( variables: RoundsQueryVariables, - chainIds: number[] + chainIds: number[], + onlywWhitelistedPrograms = false ): SWRResponse => { const dataLayer = useDataLayer(); const query = useSWR( // Cache requests on chainIds and variables as keys (when these are the // same, cache will be used instead of new requests) - ["rounds", chainIds, variables], + ["rounds", chainIds, variables, onlywWhitelistedPrograms], async () => { + const whitelistedPrograms = onlywWhitelistedPrograms + ? await getWhitelistedPrograms() + : undefined; + const [spamRounds, { rounds }] = await Promise.all([ fetchSpamRounds(), dataLayer.getRounds({ ...variables, first: 500, chainIds, + whitelistedPrograms, }), ]); @@ -64,8 +71,9 @@ const OVERRIDE_PRIVATE_ROUND_IDS = [ export const filterOutPrivateRounds = (rounds: RoundGetRound[]) => { return rounds.filter( (round) => - (round.roundMetadata && round.roundMetadata.roundType) && - round.roundMetadata.roundType !== "private" || + (round.roundMetadata && + round.roundMetadata.roundType && + round.roundMetadata.roundType !== "private") || OVERRIDE_PRIVATE_ROUND_IDS.includes(round.id.toLowerCase()) ); }; diff --git a/packages/grant-explorer/src/features/discovery/ExploreRoundsPage.tsx b/packages/grant-explorer/src/features/discovery/ExploreRoundsPage.tsx index 6da621e50d..1428ad5e1f 100644 --- a/packages/grant-explorer/src/features/discovery/ExploreRoundsPage.tsx +++ b/packages/grant-explorer/src/features/discovery/ExploreRoundsPage.tsx @@ -15,11 +15,23 @@ import { useMemo } from "react"; const ExploreRoundsPage = () => { const [params] = useSearchParams(); const filter = getRoundSelectionParamsFromUrlParams(params); - // Pass the filter from the search params and build the graphql query - const rounds = useFilterRounds(filter, getEnabledChains()); + const rounds = useFilterRounds( + filter, + getEnabledChains(), + filter.status.includes("verified") + ); - const publicRounds = useMemo(() => rounds.data?.filter(round => (round.roundMetadata && round.roundMetadata.roundType) && round.roundMetadata.roundType?.toLowerCase() !== "private"), [rounds]); + const publicRounds = useMemo( + () => + rounds.data?.filter( + (round) => + round.roundMetadata && + round.roundMetadata.roundType && + round.roundMetadata.roundType?.toLowerCase() !== "private" + ), + [rounds] + ); rounds.data = publicRounds; const sectionTitle = getExplorerPageTitle(filter); diff --git a/packages/grant-explorer/src/features/discovery/FilterDropdown.tsx b/packages/grant-explorer/src/features/discovery/FilterDropdown.tsx index b43584c12e..67581d64ca 100644 --- a/packages/grant-explorer/src/features/discovery/FilterDropdown.tsx +++ b/packages/grant-explorer/src/features/discovery/FilterDropdown.tsx @@ -41,11 +41,14 @@ export const FILTER_OPTIONS: RoundFilterUiOption[] = [ }, ], }, - { label: "Status", value: "status", children: [ + { + label: "Verified", + value: "verified", + }, { label: "Active", value: RoundStatus.active, @@ -75,7 +78,6 @@ export function FilterDropdown() { const filter = getRoundSelectionParamsFromUrlParams(params); const { status = "", type = "", network = "" } = filter; - const selected = getFilterLabel({ status, type, network }); return ( {children ?.filter((child) => !child.hide) diff --git a/packages/grant-explorer/src/features/discovery/LandingPage.tsx b/packages/grant-explorer/src/features/discovery/LandingPage.tsx index a280cc07aa..8190766bbb 100644 --- a/packages/grant-explorer/src/features/discovery/LandingPage.tsx +++ b/packages/grant-explorer/src/features/discovery/LandingPage.tsx @@ -22,11 +22,13 @@ import { CollectionsGrid } from "../collections/CollectionsGrid"; const LandingPage = () => { const activeRounds = useFilterRounds( ACTIVE_ROUNDS_FILTER, - getEnabledChains() + getEnabledChains(), + true ); const roundsEndingSoon = useFilterRounds( ROUNDS_ENDING_SOON_FILTER, - getEnabledChains() + getEnabledChains(), + true ); const filteredActiveRounds = useMemo(() => { @@ -81,7 +83,7 @@ const LandingPage = () => { View all diff --git a/packages/grant-explorer/src/features/discovery/LandingTabs.tsx b/packages/grant-explorer/src/features/discovery/LandingTabs.tsx index 40a6379685..73737d434e 100644 --- a/packages/grant-explorer/src/features/discovery/LandingTabs.tsx +++ b/packages/grant-explorer/src/features/discovery/LandingTabs.tsx @@ -31,7 +31,11 @@ export default function LandingTabs() { { to: `/rounds?${toQueryString({ orderBy: "MATCH_AMOUNT_IN_USD_DESC", - status: [RoundStatus.active, RoundStatus.taking_applications].join(","), + status: [ + RoundStatus.active, + RoundStatus.taking_applications, + "verified", + ].join(","), })}`, activeRegExp: /^\/rounds/, children: isDesktop ? "Explore rounds" : "Rounds", diff --git a/packages/grant-explorer/src/features/discovery/__tests__/LandingPage.test.tsx b/packages/grant-explorer/src/features/discovery/__tests__/LandingPage.test.tsx index 27dd7014b6..9e6bf8adda 100644 --- a/packages/grant-explorer/src/features/discovery/__tests__/LandingPage.test.tsx +++ b/packages/grant-explorer/src/features/discovery/__tests__/LandingPage.test.tsx @@ -19,6 +19,7 @@ vi.mock("common", async () => { return { ...actual, renderToPlainText: vi.fn().mockReturnValue((str = "") => str), + getWhitelistedPrograms: vi.fn().mockResolvedValue(undefined), }; }); @@ -60,7 +61,6 @@ vi.mock("wagmi", async () => { }); describe("LandingPage", () => { - it("renders landing page", () => { renderWithContext(); }); diff --git a/packages/grant-explorer/src/features/discovery/hooks/useFilterRounds.ts b/packages/grant-explorer/src/features/discovery/hooks/useFilterRounds.ts index 689b1ac0b8..5c52a0a441 100644 --- a/packages/grant-explorer/src/features/discovery/hooks/useFilterRounds.ts +++ b/packages/grant-explorer/src/features/discovery/hooks/useFilterRounds.ts @@ -57,7 +57,7 @@ export enum RoundStatus { export const ACTIVE_ROUNDS_FILTER: RoundSelectionParams = { orderBy: "MATCH_AMOUNT_IN_USD_DESC", - status: RoundStatus.active, + status: `${RoundStatus.active},verified`, type: "allov2.DonationVotingMerkleDistributionDirectTransferStrategy,allov1.QF", network: "", }; @@ -69,12 +69,13 @@ export const ROUNDS_ENDING_SOON_FILTER: RoundSelectionParams & { orderBy: "DONATIONS_END_TIME_ASC", type: "", network: "", - status: RoundStatus.ending_soon, + status: `${RoundStatus.ending_soon},verified`, }; export const useFilterRounds = ( where: RoundSelectionParams, - chains: TChain[] + chains: TChain[], + onlywWhitelistedPrograms?: boolean ): SWRResponse => { const chainIds = where.network === undefined || where.network.trim() === "" @@ -102,7 +103,7 @@ export const useFilterRounds = ( const orderBy = where.orderBy === undefined ? "CREATED_AT_BLOCK_DESC" : where.orderBy; const vars = { orderBy, filter }; - return useRounds(vars, chainIds); + return useRounds(vars, chainIds, onlywWhitelistedPrograms); }; const createRoundWhereFilter = ( diff --git a/packages/grant-explorer/src/features/discovery/utils/createRoundsStatusFilter.ts b/packages/grant-explorer/src/features/discovery/utils/createRoundsStatusFilter.ts index fe8b574f90..fc469e6d37 100644 --- a/packages/grant-explorer/src/features/discovery/utils/createRoundsStatusFilter.ts +++ b/packages/grant-explorer/src/features/discovery/utils/createRoundsStatusFilter.ts @@ -48,7 +48,7 @@ export function createRoundsStatusFilter( ): TimeFilterVariables[] { // Default to all filters const selectedFilters = - status || + status.replace(",verified", "") || [ RoundStatus.active, RoundStatus.taking_applications, diff --git a/packages/round-manager/src/assets/explorer-black.svg b/packages/round-manager/src/assets/explorer-black.svg new file mode 100644 index 0000000000..b6ca365ae9 --- /dev/null +++ b/packages/round-manager/src/assets/explorer-black.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/packages/round-manager/src/features/program/TabGroup.tsx b/packages/round-manager/src/features/program/TabGroup.tsx index e113ea9643..7decf9fd17 100644 --- a/packages/round-manager/src/features/program/TabGroup.tsx +++ b/packages/round-manager/src/features/program/TabGroup.tsx @@ -1,8 +1,12 @@ "use client"; -import { Fragment, useState, Key } from "react"; +import { Fragment, useState, Key, useEffect } from "react"; import { classNames, getStatusStyle, prettyDates3 } from "../common/Utils"; -import { PlusIcon, PlusSmIcon } from "@heroicons/react/solid"; +import { + ExclamationCircleIcon, + PlusIcon, + PlusSmIcon, +} from "@heroicons/react/solid"; import Close from "../../assets/close.svg"; import DirectGrants from "../../assets/direct-grants.svg"; import QuadraticFundingSVG from "../../assets/quadratic-funding.svg"; @@ -18,21 +22,28 @@ import { useRounds } from "../../context/round/RoundContext"; import { ProgressStatus, Round } from "../api/types"; import { Transition, Dialog } from "@headlessui/react"; import { useAccount } from "wagmi"; +import { ReactComponent as GrantExplorerLogo } from "../../assets/explorer-black.svg"; +import ConfirmationModal from "../common/ConfirmationModal"; +import { getProgramWhitelistStatus, WhitelistStatus } from "common/src"; const tabs = [ { name: "Quadratic funding", current: true }, { name: "Direct grants", current: false }, - { name: "Settings", current: false}, + { name: "Settings", current: false }, ]; export const TabGroup = () => { datadogLogs.logger.info("====> Route: /program/:id/TabGroup.tsx"); datadogLogs.logger.info(`====> URL: ${window.location.href}`); + const [openListingModal, setOpenListingModal] = useState(false); + const [whitelistStatus, setWhitelistStatus] = + useState(null); const { chainId, id: programId } = useParams() as { chainId?: string; id: string; }; + const { chain, address } = useAccount(); const programChainId = chainId ? Number(chainId) : chain?.id; const { program: programToRender } = useProgramById(programId); @@ -47,6 +58,17 @@ export const TabGroup = () => { const [isModalOpen, setIsModalOpen] = useState(false); const [currentTab, setCurrentTab] = useState(tabs[0].name); + useEffect(() => { + const getWhitelistStatus = async () => { + if (!whitelistStatus && programToRender) { + const status = await getProgramWhitelistStatus(programToRender.id!); + setWhitelistStatus(status); + } + }; + + getWhitelistStatus(); + }, [programToRender, whitelistStatus]); + const handleTabChange = (tabName: string) => { setCurrentTab(tabName); }; @@ -73,7 +95,10 @@ export const TabGroup = () => { status={status} strategyType="quadratic" displayBar={{ - applicationDate: prettyDates3(round.applicationsStartTime, round.applicationsEndTime), + applicationDate: prettyDates3( + round.applicationsStartTime, + round.applicationsEndTime + ), roundDate: prettyDates3( round.roundStartTime, round.roundEndTime @@ -312,31 +337,88 @@ export const TabGroup = () => { aria-current={tab.name === currentTab ? "page" : undefined} > {tab.name} - {["Quadratic funding", "Direct grants"].includes(tab.name) && + {["Quadratic funding", "Direct grants"].includes( + tab.name + ) && ( - {tab.name === "Quadratic funding" ? qfRounds.length : dgRounds.length} + {tab.name === "Quadratic funding" + ? qfRounds.length + : dgRounds.length} - } + )} ))}
{programToRender?.tags?.includes(getAlloVersion()) && ( - { - setIsModalOpen(true); - }} - className="flex flex-row justify-between items-center hover:shadow-md p-2 rounded-lg text-sm text-grey-500 font-mono ml-auto bg-yellow-100 cursor-pointer" - data-testid="create-round-small-link" - > - +
)} @@ -355,8 +437,11 @@ export const TabGroup = () => {
{dgRoundItems}
)} - { currentTab === "Settings" && ( - + {currentTab === "Settings" && ( + )} @@ -364,8 +449,7 @@ export const TabGroup = () => { rounds.length === 0 && programToRender?.tags?.includes(getAlloVersion()) && currentTab !== "Settings" && - noRoundsGroup - } + noRoundsGroup}