diff --git a/packages/data-layer/package.json b/packages/data-layer/package.json index 9df4dee408..67bddd6aaa 100644 --- a/packages/data-layer/package.json +++ b/packages/data-layer/package.json @@ -4,7 +4,7 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { - "dev": "tsx watch -r dotenv-flow/config src/index.ts", + "dev": "tsc --watch", "lint": "eslint --cache --max-warnings=0", "lint:fix": "eslint --cache --max-warnings=0 --fix", "lint:all:fix": "eslint --fix --cache --max-warnings=0 src", diff --git a/packages/data-layer/src/data-layer.ts b/packages/data-layer/src/data-layer.ts index b0974c6697..82536468ec 100644 --- a/packages/data-layer/src/data-layer.ts +++ b/packages/data-layer/src/data-layer.ts @@ -9,6 +9,7 @@ import * as legacy from "./backends/legacy"; import { AlloVersion, PaginationInfo } from "./data-layer.types"; import { Collection, + Program, ProjectEventsMap, Round, RoundOverview, @@ -23,6 +24,7 @@ import { SearchResult, } from "./openapi-search-client/index"; import { + getProgramsByUser, getProjectById, getProjects, getProjectsAndRolesByAddress, @@ -102,6 +104,56 @@ export class DataLayer { this.gsIndexerEndpoint = indexer.baseUrl; } + /** + * Allo v1 & v2 manager queries + */ + + /** + * Gets profiles/programs linked to an operator or user. + * + * @example + * Here is an example: + * ``` + * const program = await dataLayer.getProgramByUser({ + * address: "0x1234", + * chainId: 1, + * alloVersion: "allo-v1", + * }); + * ``` + * @param address - the address of the user. + * @param chainId - the network ID of the chain. + * @param alloVersion - the version of Allo to use. + * + * @returns Program + */ + async getProgramsByUser({ + address, + chainId, + alloVersion, + }: { + address: string; + chainId: number; + alloVersion: AlloVersion; + }): Promise<{ programs: Program[] } | null> { + const requestVariables = { + alloVersion, + address, + chainId, + }; + + const response: { projects: Program[] } = await request( + this.gsIndexerEndpoint, + getProgramsByUser, + requestVariables, + ); + + const programs = response.projects; + + if (!programs) return null; + + return { programs }; + } + /** * Allo v1 & v2 builder queries */ @@ -208,7 +260,7 @@ export class DataLayer { address: string; alloVersion: AlloVersion; chainId: number; - }): Promise { + }): Promise { const requestVariables = { address: address.toLowerCase(), version: alloVersion, @@ -221,9 +273,9 @@ export class DataLayer { requestVariables, ); - const projects = response.projects; + const projects: v2Project[] = response.projects; - if (projects.length === 0) return null; + if (projects.length === 0) return undefined; const projectEventsMap: ProjectEventsMap = {}; diff --git a/packages/data-layer/src/data.types.ts b/packages/data-layer/src/data.types.ts index b417050b68..7da2de3554 100644 --- a/packages/data-layer/src/data.types.ts +++ b/packages/data-layer/src/data.types.ts @@ -101,6 +101,23 @@ export type ProjectRole = { projectId: string; }; +export type Tags = "allo-v1" | "program update"; + +/** + * The program type for v1 + **/ + +export type Program = { + id: string; + chainId: number; + metadata: { + name: string; + }; + metadataCid?: MetadataPointer; + tags: Tags[]; + roles: AddressAndRole[]; +}; + /** * The project type for v2 * @@ -162,7 +179,7 @@ export type v2Project = { * * The tags are used to filter the projects based on the version of Allo. */ - tags: string[]; + tags: [string]; /** * The block the project was created at */ diff --git a/packages/data-layer/src/queries.ts b/packages/data-layer/src/queries.ts index d26da7e068..48c826d69b 100644 --- a/packages/data-layer/src/queries.ts +++ b/packages/data-layer/src/queries.ts @@ -1,5 +1,68 @@ import { gql } from "graphql-request"; +/** + * Get all the programs that a user is a part of + * @param $alloVersion - The version of Allo + * @param $address - The address of the user + * @param $chainId - The network ID of the chain + * + * @returns The programs + */ +export const getProgramsByUser = gql` + query ($address: String!, $chainId: Int!) { + projects( + filter: { + tags: { contains: "program" } + roles: { some: { address: { equalTo: $address } } } + and: { chainId: { equalTo: $chainId } } + } + ) { + id + chainId + metadata + metadataCid + tags + roles { + address + role + createdAtBlock + } + } + } +`; + +/** + * Get a program by its programId + * @param $alloVersion - The version of Allo + * @param $programId - The ID of the program + * @param $chainId - The network ID of the chain + * + * @returns The programs + */ +export const getProgramById = gql` + query ($alloVersion: [String!]!, $programId: String!, $chainId: Int!) { + projects( + filter: { + tags: { equalTo: $alloVersion } + tags: { contains: "program" } + id: { equalTo: $programId } + and: { chainId: { equalTo: $chainId } } + } + ) { + id + chainId + metadata + metadataCid + tags + roles { + address + role + createdAtBlock + } + } + } +`; + /** * Get a project by its ID * @param $alloVersion - The version of Allo diff --git a/packages/grant-explorer/src/features/api/rounds.ts b/packages/grant-explorer/src/features/api/rounds.ts index b71dc929d9..a87d0fbc93 100644 --- a/packages/grant-explorer/src/features/api/rounds.ts +++ b/packages/grant-explorer/src/features/api/rounds.ts @@ -2,43 +2,10 @@ import useSWR, { useSWRConfig, Cache, SWRResponse } from "swr"; import { ChainId, RoundPayoutType } from "common"; import { __deprecated_RoundMetadata } from "./round"; import { MetadataPointer } from "./types"; -import { __deprecated_fetchFromIPFS, useDebugMode } from "./utils"; +import { __deprecated_fetchFromIPFS } from "./utils"; import { createTimestamp } from "../discovery/utils/createRoundsStatusFilter"; import { useDataLayer } from "data-layer"; -const validRounds = [ - "0x35c9d05558da3a3f3cddbf34a8e364e59b857004", // "Metacamp Onda 2023 FINAL - "0x984e29dcb4286c2d9cbaa2c238afdd8a191eefbc", // Gitcoin Citizens Round #1 - "0x4195cd3cd76cc13faeb94fdad66911b4e0996f38", // Greenpill Q2 2023 -].map((a) => a.toLowerCase()); - -const invalidRounds = [ - "0xde272b1a1efaefab2fd168c02b8cf0e3b10680ef", // Meg hello - - // https://github.com/gitcoinco/grants-stack/issues/2569 - "0x7c1104c39e09e7c6114f3d4e30a180a714deac7d", - "0x79715bf10dab457e06020ec41efae3484cff59dc", - "0x4632ea15ba3c1a7e072996fb316efefb8280381b", - "0xfe36ff9c59788a6a9ad7a979f459d69372dad0e6", - "0xa7149a073db99cd9ac267daf0c4f7767e50acf3f", - "0x4abc6f3322158bcec933f18998709322de7152c2", - "0xae53557089a1d771cd5cebeaf6accbe8f064ff4c", - "0xee3ed186939af2c55d33d242c4588426e368c8d0", - "0x8011e814439b44aa340bc3373df06233f45e3202", - "0xf3cd7429e863a39a9ecab60adc4676c1934076f2", - "0x88fc9d6695bedd34bbbe4ea0e2510573200713c7", - "0xae18f327ce481a7316d28a625d4c378c1f8b03a2", - "0x9b3b1e7edf9c5eea07fb3c7270220be1c3fea111", - "0x4c19261ff6e5736a2677a06741bf1e68995e7c95", - "0x1ebac14c3b3e539b0c1334415c70a923eb7c736f", - "0x3979611e7ca6db8f45b4a768079a88d9138622c1", - "0x0b1e3459cdadc52fca8977cede34f28bc298e3df", - "0x1427a0e71a222b0229a910dc72da01f8f04c7441", - "0xc25994667632d55a8e3dae88737e36f496600434", - "0x21d264139d66dd281dcb0177bbdca5ceeb71ad69", - "0x822742805c0596e883aba99ba2f3117e8c49b94a", -].map((a) => a.toLowerCase()); - export type __deprecated_RoundOverview = { id: string; chainId: ChainId; @@ -94,7 +61,6 @@ export const useRounds = ( chainIds: ChainId[] ): SWRResponse<__deprecated_RoundOverview[]> => { const { cache, mutate } = useSWRConfig(); - const isDebugModeEnabled = useDebugMode(); const dataLayer = useDataLayer(); const prewarmSwrCacheWithRoundsMetadata = async ( @@ -140,12 +106,14 @@ export const useRounds = ( await prewarmSwrCacheWithRoundsMetadata(rounds); return rounds; + }, + { + revalidateOnFocus: false, + revalidateIfStale: false, } ); - const data = ( - isDebugModeEnabled ? query.data : filterRounds(cache, query.data) - ) + const data = query.data // Limit final results returned ?.slice(0, variables.first ?? query.data?.length); @@ -169,14 +137,6 @@ export const filterRounds = ( rounds?: __deprecated_RoundOverview[] ) => { return rounds?.filter((round) => { - if (validRounds.includes(round.id.toLowerCase())) { - return true; - } - - if (invalidRounds.includes(round.id.toLowerCase())) { - return false; - } - // Get the round metadata const metadata = cache.get(`@"metadata","${round.roundMetaPtr.pointer}",`); if (metadata?.data?.roundType === "public") { diff --git a/packages/grant-explorer/src/features/discovery/LandingPage.tsx b/packages/grant-explorer/src/features/discovery/LandingPage.tsx index 311bc2236e..ac5b87bad8 100644 --- a/packages/grant-explorer/src/features/discovery/LandingPage.tsx +++ b/packages/grant-explorer/src/features/discovery/LandingPage.tsx @@ -10,6 +10,7 @@ import { } from "./hooks/useFilterRounds"; import { toQueryString } from "./RoundsFilter"; import { getEnabledChains } from "../../app/chainConfig"; +import { useMemo } from "react"; const LandingPage = () => { const activeRounds = useFilterRounds( @@ -21,6 +22,23 @@ const LandingPage = () => { getEnabledChains() ); + const filteredActiveRounds: typeof activeRounds.data = useMemo(() => { + if (activeRounds.data === undefined) { + return undefined; + } + + const rounds = + activeRounds.data?.filter((round) => { + return (round.projects?.length ?? 0) > 1; + }) ?? []; + + rounds.sort((a, b) => { + return (b.projects?.length ?? 0) - (a.projects?.length ?? 0); + }); + + return rounds; + }, [activeRounds.data]); + return ( @@ -33,7 +51,7 @@ const LandingPage = () => { } > diff --git a/packages/grant-explorer/src/features/discovery/hooks/__tests__/useFilterRounds.test.tsx b/packages/grant-explorer/src/features/discovery/hooks/__tests__/useFilterRounds.test.tsx index 9e922f2d22..b086a483fc 100644 --- a/packages/grant-explorer/src/features/discovery/hooks/__tests__/useFilterRounds.test.tsx +++ b/packages/grant-explorer/src/features/discovery/hooks/__tests__/useFilterRounds.test.tsx @@ -109,26 +109,5 @@ describe("useFilterRounds", () => { )?.length ).toBe(5); expect(filterRounds(cacheMock, MOCKED_ROUNDS)?.length).toBe(0); - - expect( - filterRounds( - cacheMock, - MOCKED_ROUNDS.map((r) => ({ - ...r, - // If RoundID is part of valid rounds - id: "0x35c9d05558da3a3f3cddbf34a8e364e59b857004", - })) - )?.length - ).toBe(5); - expect( - filterRounds( - cacheMock, - MOCKED_ROUNDS.map((r) => ({ - ...r, - // If RoundID is part of invalid rounds - id: "0xde272b1a1efaefab2fd168c02b8cf0e3b10680ef", - })) - )?.length - ).toBe(0); }); }); diff --git a/packages/round-manager/package.json b/packages/round-manager/package.json index 62ffdbc118..72a2103d93 100644 --- a/packages/round-manager/package.json +++ b/packages/round-manager/package.json @@ -54,6 +54,7 @@ "allo-indexer-client": "github:gitcoinco/allo-indexer-client", "buffer": "^6.0.3", "common": "workspace:*", + "data-layer": "workspace:*", "crypto-browserify": "^3.12.0", "csv-parse": "^5.3.8", "csv-stringify": "^6.3.2", diff --git a/packages/round-manager/src/context/program/ReadProgramContext.tsx b/packages/round-manager/src/context/program/ReadProgramContext.tsx index 3a5d79d4a5..9a9e7a8522 100644 --- a/packages/round-manager/src/context/program/ReadProgramContext.tsx +++ b/packages/round-manager/src/context/program/ReadProgramContext.tsx @@ -8,6 +8,7 @@ import { useWallet } from "../../features/common/Auth"; import { getProgramById, listPrograms } from "../../features/api/program"; import { datadogLogs } from "@datadog/browser-logs"; import { Web3Provider } from "@ethersproject/providers"; +import { DataLayer, useDataLayer } from "data-layer"; export interface ReadProgramState { programs: Program[]; @@ -45,6 +46,7 @@ export const ReadProgramContext = createContext(undefined); const fetchProgramsByAddress = async ( dispatch: Dispatch, address: string, + dataLayer: DataLayer, walletProvider: Web3Instance["provider"] ) => { datadogLogs.logger.info(`fetchProgramsByAddress: address - ${address}`); @@ -54,7 +56,7 @@ const fetchProgramsByAddress = async ( payload: ProgressStatus.IN_PROGRESS, }); try { - const programs = await listPrograms(address, walletProvider); + const programs = await listPrograms(address, walletProvider, dataLayer); dispatch({ type: ActionType.SET_PROGRAMS, payload: programs }); dispatch({ type: ActionType.SET_FETCH_PROGRAM_STATUS, @@ -149,9 +151,15 @@ export const usePrograms = (): ReadProgramState & { dispatch: Dispatch } => { } const { address, provider: walletProvider } = useWallet(); + const dataLayer = useDataLayer(); useEffect(() => { - fetchProgramsByAddress(context.dispatch, address, walletProvider); + fetchProgramsByAddress( + context.dispatch, + address.toLowerCase(), + dataLayer, + walletProvider + ); }, [address, walletProvider]); // eslint-disable-line react-hooks/exhaustive-deps return { ...context.state, dispatch: context.dispatch }; diff --git a/packages/round-manager/src/context/program/__tests__/CreateProgramContext.test.tsx b/packages/round-manager/src/context/program/__tests__/CreateProgramContext.test.tsx index 5d7872a37f..a0c54cc7de 100644 --- a/packages/round-manager/src/context/program/__tests__/CreateProgramContext.test.tsx +++ b/packages/round-manager/src/context/program/__tests__/CreateProgramContext.test.tsx @@ -28,6 +28,11 @@ jest.mock("wagmi"); jest.mock("@rainbow-me/rainbowkit", () => ({ ConnectButton: jest.fn(), })); +jest.mock("data-layer", () => ({ + useDataLayer: () => ({ + getProgramsByUser: jest.fn(), + }), +})); describe("", () => { beforeEach(() => { diff --git a/packages/round-manager/src/context/program/__tests__/ReadProgramContext.test.tsx b/packages/round-manager/src/context/program/__tests__/ReadProgramContext.test.tsx index 6ddd5b4865..a96032a755 100644 --- a/packages/round-manager/src/context/program/__tests__/ReadProgramContext.test.tsx +++ b/packages/round-manager/src/context/program/__tests__/ReadProgramContext.test.tsx @@ -25,6 +25,11 @@ jest.mock("wagmi"); jest.mock("@rainbow-me/rainbowkit", () => ({ ConnectButton: jest.fn(), })); +jest.mock("data-layer", () => ({ + useDataLayer: () => ({ + getProgramsByUser: jest.fn(), + }), +})); describe("", () => { beforeEach(() => { diff --git a/packages/round-manager/src/features/api/__tests__/program.test.ts b/packages/round-manager/src/features/api/__tests__/program.test.ts index df83533c1b..94a9bd9622 100644 --- a/packages/round-manager/src/features/api/__tests__/program.test.ts +++ b/packages/round-manager/src/features/api/__tests__/program.test.ts @@ -15,46 +15,43 @@ jest.mock("common", () => ({ graphql_fetch: jest.fn(), })); +jest.mock("data-layer", () => ({ + DataLayer: jest.fn().mockImplementation(() => ({ + getProgramsByUser: jest.fn().mockResolvedValue({ + programs: [], + }), + })), +})); + describe("listPrograms", () => { - it("calls the graphql endpoint and maps the metadata from IPFS", async () => { + it("calls the indexer endpoint", async () => { // const address = "0x0" const expectedProgram = makeProgramData({ chain: CHAINS[ChainId.MAINNET], }); const expectedPrograms: Program[] = [expectedProgram]; - (graphql_fetch as jest.Mock).mockResolvedValue({ - data: { - programs: [ - { - id: expectedProgram.id, - roles: [ - { - accounts: [ - { - address: expectedProgram.operatorWallets[0], - }, - ], + + const actualPrograms = await listPrograms( + "0x0", + { + getNetwork: async () => + // @ts-expect-error Test file + Promise.resolve({ chainId: ChainId.MAINNET }), + }, + { + getProgramsByUser: jest.fn().mockResolvedValue({ + programs: [ + { + id: expectedProgram.id, + roles: [{ address: expectedProgram.operatorWallets[0] }], + metadata: { + name: expectedProgram.metadata?.name, }, - ], - metaPtr: { - protocol: 1, - pointer: - "uwijkhxkpkdgkszraqzqvhssqulctxzvntxwconznfkelzbtgtqysrzkehl", }, - }, - ], - }, - }); - - (fetchFromIPFS as jest.Mock).mockResolvedValue({ - name: expectedProgram.metadata?.name, - }); - - const actualPrograms = await listPrograms("0x0", { - getNetwork: async () => - // @ts-expect-error Test file - Promise.resolve({ chainId: ChainId.MAINNET }), - }); + ], + }), + } + ); expect(actualPrograms).toEqual(expectedPrograms); }); diff --git a/packages/round-manager/src/features/api/program.ts b/packages/round-manager/src/features/api/program.ts index ceaee4d983..056316ca09 100644 --- a/packages/round-manager/src/features/api/program.ts +++ b/packages/round-manager/src/features/api/program.ts @@ -5,6 +5,8 @@ import { ethers } from "ethers"; import { datadogLogs } from "@datadog/browser-logs"; import { Signer } from "@ethersproject/abstract-signer"; import { ChainId, graphql_fetch } from "common"; +import { DataLayer } from "data-layer"; +import { getConfig } from "common/src/config"; /** * Fetch a list of programs @@ -14,7 +16,8 @@ import { ChainId, graphql_fetch } from "common"; */ export async function listPrograms( address: string, - signerOrProvider: Web3Instance["provider"] + signerOrProvider: Web3Instance["provider"], + dataLayer: DataLayer ): Promise { try { // fetch chain id @@ -22,44 +25,28 @@ export async function listPrograms( chainId: ChainId; }; - // get the subgraph for all programs owned by the given address - const res = await graphql_fetch( - ` - query GetPrograms($address: String!) { - programs(where: { - accounts_: { - address: $address - } - }) { - id - metaPtr { - protocol - pointer - } - roles(where: { - role: "0xaa630204f2780b6f080cc77cc0e9c0a5c21e92eb0c6771e709255dd27d6de132" - }) { - accounts { - address - } - } - } - } - `, - chainId, - { address: address.toLowerCase() } - ); + const config = getConfig(); - const programs: Program[] = []; + // fetch programs from indexer + + const programsRes = await dataLayer.getProgramsByUser({ + address: address, + chainId: chainId, + alloVersion: config.allo.version, + }); - for (const program of res.data.programs) { - const metadata = await fetchFromIPFS(program.metaPtr.pointer); + if (!programsRes) { + throw Error("Unable to fetch programs"); + } + + const programs: Program[] = []; + for (const program of programsRes.programs) { programs.push({ id: program.id, - metadata, - operatorWallets: program.roles[0].accounts.map( - (account: { address: string }) => account.address + metadata: program.metadata, + operatorWallets: program.roles.map( + (role: { address: string }) => role.address ), chain: { id: chainId, diff --git a/packages/round-manager/src/features/program/__tests__/CreateProgramPage.test.tsx b/packages/round-manager/src/features/program/__tests__/CreateProgramPage.test.tsx index c850d5e249..5aaff16fbd 100644 --- a/packages/round-manager/src/features/program/__tests__/CreateProgramPage.test.tsx +++ b/packages/round-manager/src/features/program/__tests__/CreateProgramPage.test.tsx @@ -28,6 +28,12 @@ jest.mock("../../../constants", () => ({ errorModalDelayMs: 0, // NB: use smaller delay for faster tests })); +jest.mock("data-layer", () => ({ + useDataLayer: () => ({ + getProgramsByUser: jest.fn(), + }), +})); + describe("", () => { let consoleErrorSpy: jest.SpyInstance; diff --git a/packages/round-manager/src/features/program/__tests__/ListProgramPage.test.tsx b/packages/round-manager/src/features/program/__tests__/ListProgramPage.test.tsx index 928f24c351..4b0f2f4cfb 100644 --- a/packages/round-manager/src/features/program/__tests__/ListProgramPage.test.tsx +++ b/packages/round-manager/src/features/program/__tests__/ListProgramPage.test.tsx @@ -14,6 +14,11 @@ jest.mock("wagmi"); jest.mock("@rainbow-me/rainbowkit", () => ({ ConnectButton: jest.fn(), })); +jest.mock("data-layer", () => ({ + useDataLayer: () => ({ + getProgramsByUser: jest.fn(), + }), +})); describe("", () => { it("does not render a list of programs when no programs have been created", () => { diff --git a/packages/round-manager/src/features/program/__tests__/ViewProgramPage.test.tsx b/packages/round-manager/src/features/program/__tests__/ViewProgramPage.test.tsx index 80f535a5ad..0b97446b67 100644 --- a/packages/round-manager/src/features/program/__tests__/ViewProgramPage.test.tsx +++ b/packages/round-manager/src/features/program/__tests__/ViewProgramPage.test.tsx @@ -26,6 +26,12 @@ jest.mock("react-router-dom", () => ({ useParams: useParamsFn, })); +jest.mock("data-layer", () => ({ + useDataLayer: () => ({ + getProgramsByUser: jest.fn(), + }), +})); + describe("", () => { let stubProgram: Program; diff --git a/packages/round-manager/src/index.tsx b/packages/round-manager/src/index.tsx index b919206e1a..56317659a8 100644 --- a/packages/round-manager/src/index.tsx +++ b/packages/round-manager/src/index.tsx @@ -34,6 +34,8 @@ import ViewApplication from "./features/round/ViewApplicationPage"; import ViewRoundPage from "./features/round/ViewRoundPage"; import { initSentry } from "./sentry"; import { UpdateRoundProvider } from "./context/round/UpdateRoundContext"; +import { DataLayer, DataLayerProvider } from "data-layer"; +import { getConfig } from "common/src/config"; // Initialize sentry initSentry(); @@ -48,96 +50,113 @@ const root = ReactDOM.createRoot( document.getElementById("root") as HTMLElement ); +const dataLayerConfig = new DataLayer({ + search: { + baseUrl: getConfig().dataLayer.searchServiceBaseUrl, + pagination: { + pageSize: 50, + }, + }, + subgraph: { + endpointsByChainId: getConfig().dataLayer.subgraphEndpoints, + }, + indexer: { + baseUrl: `${getConfig().dataLayer.gsIndexerEndpoint}/graphql`, + }, +}); + root.render( - - - {/* Protected Routes */} - }> - {/* Default Route */} - - - - } - /> - - {/* Round Routes */} - - - - - - } - /> - - - - - - - - - - - - - - - - } - /> - - - - - - - - } - /> + + + + {/* Protected Routes */} + }> + {/* Default Route */} + + + + } + /> - {/* Program Routes */} - - - - } - /> - + {/* Round Routes */} + - + + + - - } - /> + } + /> + + + + + + + + + + + + + + + + } + /> + + + + + + + + } + /> + + {/* Program Routes */} + + + + } + /> + + + + + + } + /> - {/* Access Denied */} - } /> + {/* Access Denied */} + } /> - {/* 404 */} - } /> - - - + {/* 404 */} + } /> + + + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fd35b9ba8d..4c09b6aa7f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -985,6 +985,9 @@ importers: csv-stringify: specifier: ^6.3.2 version: 6.4.2 + data-layer: + specifier: workspace:* + version: link:../data-layer date-fns: specifier: ^2.29.3 version: 2.30.0