diff --git a/app/__tests__/components/ExpiredStampModal.test.tsx b/app/__tests__/components/ExpiredStampModal.test.tsx index 2b20cf73a3..5200841b7c 100644 --- a/app/__tests__/components/ExpiredStampModal.test.tsx +++ b/app/__tests__/components/ExpiredStampModal.test.tsx @@ -48,8 +48,6 @@ describe("ExpiredStampModal", () => { "GitcoinContributorStatistics#totalContributionAmountGte#10", "GitcoinContributorStatistics#totalContributionAmountGte#100", "GitcoinContributorStatistics#totalContributionAmountGte#1000", - "GitcoinContributorStatistics#numGr14ContributionsGte#1", - "GitcoinContributorStatistics#numRoundsContributedToGte#1", ]); }); it("should delete all stamps within each expired platform", async () => { diff --git a/app/context/ceramicContext.tsx b/app/context/ceramicContext.tsx index a028e6ec5e..76fa6cc4f1 100644 --- a/app/context/ceramicContext.tsx +++ b/app/context/ceramicContext.tsx @@ -43,7 +43,6 @@ const { Idena, Civic, CyberConnect, - GrantsStack, TrustaLabs, } = stampPlatforms; import { PlatformProps } from "../components/GenericPlatform"; @@ -228,11 +227,6 @@ if (process.env.NEXT_PUBLIC_FF_CYBERCONNECT_STAMPS === "on") { }); } -platforms.set("GrantsStack", { - platform: new GrantsStack.GrantsStackPlatform(), - platFormGroupSpec: GrantsStack.ProviderConfig, -}); - if (process.env.NEXT_PUBLIC_FF_TRUSTALABS_STAMPS === "on") { platforms.set("TrustaLabs", { platform: new TrustaLabs.TrustaLabsPlatform(), diff --git a/platforms/src/Gitcoin/App-Bindings.ts b/platforms/src/Gitcoin/App-Bindings.ts index a35c747411..9b2274b5f3 100644 --- a/platforms/src/Gitcoin/App-Bindings.ts +++ b/platforms/src/Gitcoin/App-Bindings.ts @@ -1,15 +1,15 @@ -import { PlatformOptions, ProviderPayload } from "../types"; +import { AppContext, PlatformOptions, ProviderPayload } from "../types"; import { Platform } from "../utils/platform"; export class GitcoinPlatform extends Platform { platformId = "Gitcoin"; - path = "github"; + path = "Gitcoin"; clientId: string = null; redirectUri: string = null; + isEVM = true; banner = { - heading: - "The Gitcoin Grant stamp recognizes contributions made during Gitcoin Grants rounds 1-15 (ended September 2022). Please use the new GrantsStack stamp for donations from the latest rounds: Alpha (Jan '23) and Beta (April '23).", + heading: "Note: Verification for the Gitcoin Grants stamp only considers matching-eligible contributions.", }; constructor(options: PlatformOptions = {}) { @@ -18,10 +18,8 @@ export class GitcoinPlatform extends Platform { this.redirectUri = options.redirectUri as string; } - async getOAuthUrl(state: string): Promise { - const githubUrl = await Promise.resolve( - `https://github.com/login/oauth/authorize?client_id=${this.clientId}&redirect_uri=${this.redirectUri}&state=${state}` - ); - return githubUrl; + async getProviderPayload(_appContext: AppContext): Promise { + const result = await Promise.resolve({}); + return result; } } diff --git a/platforms/src/Gitcoin/Providers-config.ts b/platforms/src/Gitcoin/Providers-config.ts index f4a14b3cb7..9f8556affc 100644 --- a/platforms/src/Gitcoin/Providers-config.ts +++ b/platforms/src/Gitcoin/Providers-config.ts @@ -28,13 +28,6 @@ export const ProviderConfig: PlatformGroupSpec[] = [ { title: "at least $1000", name: "GitcoinContributorStatistics#totalContributionAmountGte#1000" }, ], }, - { - platformGroup: "Contributed in...", - providers: [ - { title: "GR14", name: "GitcoinContributorStatistics#numGr14ContributionsGte#1" }, - { title: "at least 1 Round", name: "GitcoinContributorStatistics#numRoundsContributedToGte#1" }, - ], - }, ]; export const providers: Provider[] = [ @@ -74,14 +67,4 @@ export const providers: Provider[] = [ receivingAttribute: "total_contribution_amount", recordAttribute: "totalContributionAmountGte", }), - new GitcoinContributorStatisticsProvider({ - threshold: 1, - receivingAttribute: "num_rounds_contribute_to", - recordAttribute: "numRoundsContributedToGte", - }), - new GitcoinContributorStatisticsProvider({ - threshold: 1, - receivingAttribute: "num_gr14_contributions", - recordAttribute: "numGr14ContributionsGte", - }), ]; diff --git a/platforms/src/Gitcoin/Providers/__tests__/gitcoinGrantsStatistics.test.ts b/platforms/src/Gitcoin/Providers/__tests__/gitcoinGrantsStatistics.test.ts index babb0695c6..1c4de2672c 100644 --- a/platforms/src/Gitcoin/Providers/__tests__/gitcoinGrantsStatistics.test.ts +++ b/platforms/src/Gitcoin/Providers/__tests__/gitcoinGrantsStatistics.test.ts @@ -12,14 +12,13 @@ jest.mock("axios"); const mockedAxios = axios as jest.Mocked; const userHandle = "my-login-handle"; -const githubId = "18723656"; -const clientId = process.env.GITHUB_CLIENT_ID; -const clientSecret = process.env.GITHUB_CLIENT_SECRET; const cgrantsApiToken = process.env.CGRANTS_API_TOKEN; +const address = "0x0"; + const validGithubUserResponse = { data: { - id: githubId, + address, login: userHandle, type: "User", }, @@ -76,9 +75,9 @@ describe("Attempt verification %s", function () { ["num_grants_contribute_to", "numGrantsContributedToGte", 123, 122, false], ["num_grants_contribute_to", "numGrantsContributedToGte", 123, 123, true], ["num_grants_contribute_to", "numGrantsContributedToGte", 123, 124, true], - ["num_rounds_contribute_to", "numRoundsContributedToGte", 12, 11, false], - ["num_rounds_contribute_to", "numRoundsContributedToGte", 12, 12, true], - ["num_rounds_contribute_to", "numRoundsContributedToGte", 12, 13, true], + ["total_contribution_amount", "totalContributionAmountGte", 12, 11, false], + ["total_contribution_amount", "totalContributionAmountGte", 12, 12, true], + ["total_contribution_amount", "totalContributionAmountGte", 12, 13, true], ])( " for %p (and VerifiedPayload record %p) with threshold %p for the received value is %p expects %p", async ( @@ -89,8 +88,7 @@ describe("Attempt verification %s", function () { expectedValid: boolean ) => { (axios.get as jest.Mock).mockImplementation((url) => { - if (url === "https://api.github.com/user") return Promise.resolve(validGithubUserResponse); - else if (url.includes(testDataUrlPath)) + if (url.includes(testDataUrlPath)) return Promise.resolve({ status: 200, data: { @@ -112,7 +110,7 @@ describe("Attempt verification %s", function () { }); const gitcoinPayload = await gitcoin.verify( { - address: "0x0", + address, proofs: { code, }, @@ -120,24 +118,10 @@ describe("Attempt verification %s", function () { {} ); - expect(axios.post).toHaveBeenCalledTimes(1); - // Check the request to get the token - expect(mockedAxios.post).toBeCalledWith( - `https://github.com/login/oauth/access_token?client_id=${clientId}&client_secret=${clientSecret}&code=${code}`, - {}, - { - headers: { Accept: "application/json" }, - } - ); - - expect(axios.get).toHaveBeenCalledTimes(2); - // Check the request to get the user - expect(mockedAxios.get).toBeCalledWith("https://api.github.com/user", { - headers: { Authorization: `token ${githubAccessCode}` }, - }); + expect(axios.get).toHaveBeenCalledTimes(1); // Check the request to get the contribution stats - expect(mockedAxios.get).toBeCalledWith(`${testUrl}?github_id=${githubId}`, { + expect(mockedAxios.get).toBeCalledWith(`${testUrl}?address=${address}`, { headers: { Authorization: cgrantsApiToken }, }); @@ -145,7 +129,7 @@ describe("Attempt verification %s", function () { expect(gitcoinPayload).toEqual({ valid: true, record: { - id: validGithubUserResponse.data.id, + address, [recordAttribute]: `${threshold}`, }, errors: [], @@ -154,280 +138,42 @@ describe("Attempt verification %s", function () { expect(gitcoinPayload).toEqual({ valid: false, record: undefined, - errors: [`You do not qualify for this stamp. Your Grantee stats are less than the required thresholds: ${returnedValue} out of ${threshold}.`], + errors: [ + `You do not qualify for this stamp. Your Grantee stats are less than the required thresholds: ${returnedValue} out of ${threshold}.`, + ], }); } ); - - it("should throw Provider Verification error when unable to retrieve auth token (http status code 500 received)", async () => { - (axios.post as jest.Mock).mockImplementation(async () => { - return { + it("should gracefully handle error responses from the scorer API", async () => { + const error = "Error"; + (axios.get as jest.Mock).mockRejectedValue({ + message: error, + response: { status: 500, - }; - }); - - (axios.get as jest.Mock).mockImplementation((url) => { - return Promise.resolve({ - status: 200, + statusText: "Internal Server Error", data: {}, - }); - }); - - const github = new GitcoinGrantStatisticsProviderTester({ threshold: 1 }); - - await expect(async () => { - return await github.verify( - { - address: "0x0", - proofs: { - code, - }, - } as unknown as RequestPayload, - {} - ); - }).rejects.toThrow(new ProviderExternalVerificationError("Gitcoin Grants Statistic verification error: TypeError: Cannot read properties of undefined (reading 'access_token').")); - - expect(axios.post).toHaveBeenCalledTimes(1); - expect(axios.get).toHaveBeenCalledTimes(0); - }); - - it("should throw Provider External Verification error when unable to retrieve auth token (exception thrown)", async () => { - (axios.post as jest.Mock).mockImplementation(async () => { - throw "Some kind of error"; - }); - - (axios.get as jest.Mock).mockImplementation((url) => { - return Promise.resolve({ - status: 200, - data: {}, - }); - }); - - const github = new GitcoinGrantStatisticsProviderTester({ threshold: 1 }); - - await expect(async () => { - return await github.verify( - { - address: "0x0", - proofs: { - code, - }, - } as unknown as RequestPayload, - {} - ); - }).rejects.toThrow(new ProviderExternalVerificationError("Gitcoin Grants Statistic verification error: Some kind of error.")); - - expect(axios.post).toHaveBeenCalledTimes(1); - expect(axios.get).toHaveBeenCalledTimes(0); - }); - - it("should return invalid payload when there is no id in verifyGithub response", async () => { - (axios.get as jest.Mock).mockImplementation((url) => { - if (url === "https://api.github.com/user") - return Promise.resolve({ - ...validGithubUserResponse, - data: { - // no id set here - login: userHandle, - type: "User", - }, - }); - else if (url.startsWith("https://bounties.gitcoin.co/grants/v1/api/vc/contributor_statistics")) - return Promise.resolve({ - status: 200, - data: { - ...{ - num_grants_contribute_to: 0, - num_rounds_contribute_to: 0, - total_contribution_amount: 0, - num_gr14_contributions: false, - }, - }, - }); + }, }); - const github = new GitcoinGrantStatisticsProviderTester({ threshold: 1 }); - - const gitcoinPayload = await github.verify( - { - address: "0x0", - proofs: { - code, - }, - } as unknown as RequestPayload, - {} - ); - - expect(axios.post).toHaveBeenCalledTimes(1); - // Check the request to get the token - expect(mockedAxios.post).toBeCalledWith( - `https://github.com/login/oauth/access_token?client_id=${clientId}&client_secret=${clientSecret}&code=${code}`, - {}, - { - headers: { Accept: "application/json" }, - } - ); - - expect(axios.get).toHaveBeenCalledTimes(1); - // Check the request to get the user - expect(mockedAxios.get).toBeCalledWith("https://api.github.com/user", { - headers: { Authorization: `token ${githubAccessCode}` }, - }); - expect(gitcoinPayload).toMatchObject({ valid: false }); - }); - - it("should throw Provider External Verification error when a bad status code is returned by github user api", async () => { - (axios.get as jest.Mock).mockImplementation((url) => { - if (url === "https://api.github.com/user") throw new Error("API EXCEPTION"); - else if (url.startsWith("https://bounties.gitcoin.co/grants/v1/api/vc/contributor_statistics")) - return Promise.resolve({ - status: 200, - data: { - ...{ - num_grants_contribute_to: 0, - num_rounds_contribute_to: 0, - total_contribution_amount: 0, - num_gr14_contributions: false, - }, - }, - }); - }); - - const github = new GitcoinGrantStatisticsProviderTester({ threshold: 1 }); - - await expect(async () => { - return await github.verify( - { - address: "0x0", - proofs: { - code, - }, - } as unknown as RequestPayload, - {} - ); - }).rejects.toThrow(new ProviderExternalVerificationError("Gitcoin Grants Statistic verification error: Error: API EXCEPTION.")); - - expect(axios.post).toHaveBeenCalledTimes(1); - // Check the request to get the token - expect(mockedAxios.post).toBeCalledWith( - `https://github.com/login/oauth/access_token?client_id=${clientId}&client_secret=${clientSecret}&code=${code}`, - {}, - { - headers: { Accept: "application/json" }, - } - ); - - expect(axios.get).toHaveBeenCalledTimes(1); - // Check the request to get the user - expect(mockedAxios.get).toBeCalledWith("https://api.github.com/user", { - headers: { Authorization: `token ${githubAccessCode}` }, - }); - }); - - it("should throw Provider External Verification error when a bad response received when calling the github user api (exception thrown)", async () => { - (axios.get as jest.Mock).mockImplementation((url) => { - if (url === "https://api.github.com/user") throw "Some kind of error"; - else if (url.startsWith("https://bounties.gitcoin.co/grants/v1/api/vc/contributor_statistics")) - return Promise.resolve({ - status: 200, - data: { - ...{ - num_grants_contribute_to: 0, - num_rounds_contribute_to: 0, - total_contribution_amount: 0, - num_gr14_contributions: false, - }, - }, - }); - }); - - const github = new GitcoinGrantStatisticsProviderTester({ threshold: 1 }); - - await expect(async () => { - return await github.verify( - { - address: "0x0", - proofs: { - code, - }, - } as unknown as RequestPayload, - {} - ); - }).rejects.toThrow(new ProviderExternalVerificationError("Gitcoin Grants Statistic verification error: Some kind of error.")); - - expect(axios.post).toHaveBeenCalledTimes(1); - // Check the request to get the token - expect(mockedAxios.post).toBeCalledWith( - `https://github.com/login/oauth/access_token?client_id=${clientId}&client_secret=${clientSecret}&code=${code}`, - {}, - { - headers: { Accept: "application/json" }, - } - ); - - expect(axios.get).toHaveBeenCalledTimes(1); - // Check the request to get the user - expect(mockedAxios.get).toBeCalledWith("https://api.github.com/user", { - headers: { Authorization: `token ${githubAccessCode}` }, + const gitcoin = new GitcoinGrantStatisticsProviderTester({ + threshold: 1000, + receivingAttribute: "total_contribution_amount", + recordAttribute: "totalContributionAmountGte", }); - }); - it("should use the lowercase github handle when making querying the gitcoin API and throw error if not", async () => { - (axios.get as jest.Mock).mockImplementation((url) => { - if (url === "https://api.github.com/user") { - return Promise.resolve({ - data: { - id: "18723656", - login: "User-Handle-With-Upper", - type: "User", - }, - status: 200, - }); - } else if (url.startsWith("https://bounties.gitcoin.co/grants/v1/api/vc/contributor_statistics")) - return Promise.resolve({ - status: 200, - data: { - ...{ - num_grants_contribute_to: 0, - num_rounds_contribute_to: 0, - total_contribution_amount: 0, - num_gr14_contributions: false, + await expect( + async () => + await gitcoin.verify( + { + address, + proofs: { + code, }, - }, - }); - }); - - const github = new GitcoinGrantStatisticsProviderTester({ threshold: 1 }); - - await expect(async () => { - return await github.verify( - { - address: "0x0", - proofs: { - code, - }, - } as unknown as RequestPayload, - {} - ); - }).rejects.toThrowError("Gitcoin Grants Statistic verification error"); - expect(axios.post).toHaveBeenCalledTimes(1); - // Check the request to get the token - expect(mockedAxios.post).toBeCalledWith( - `https://github.com/login/oauth/access_token?client_id=${clientId}&client_secret=${clientSecret}&code=${code}`, - {}, - { - headers: { Accept: "application/json" }, - } + } as unknown as RequestPayload, + {} + ) + ).rejects.toThrow( + "Gitcoin Grants Statistic verification error: ProviderExternalVerificationError: Error getting user info: Error - Status 500: Internal Server Error - Details: {}." ); - - expect(axios.get).toHaveBeenCalledTimes(2); - - // Check the request to get the user - expect(mockedAxios.get).toBeCalledWith("https://api.github.com/user", { - headers: { Authorization: `token ${githubAccessCode}` }, - }); - expect(mockedAxios.get).nthCalledWith(2, `${testUrl}?github_id=18723656`, { - headers: { Authorization: cgrantsApiToken }, - }); }); }); diff --git a/platforms/src/Gitcoin/Providers/gitcoinGrantsStatistics.ts b/platforms/src/Gitcoin/Providers/gitcoinGrantsStatistics.ts index 2dc1138b9f..10755b72f3 100644 --- a/platforms/src/Gitcoin/Providers/gitcoinGrantsStatistics.ts +++ b/platforms/src/Gitcoin/Providers/gitcoinGrantsStatistics.ts @@ -3,7 +3,6 @@ import type { ProviderContext, RequestPayload, VerifiedPayload } from "@gitcoin/ import { ProviderExternalVerificationError, type Provider, type ProviderOptions } from "../../types"; import { ProviderError } from "../../utils/errors"; import axios from "axios"; -import { getGithubUserData, GithubUserMetaData } from "../../Github/Providers/githubClient"; export type GitcoinGrantStatistics = { error?: string | undefined; @@ -21,7 +20,7 @@ export class GitcoinGrantStatisticsProvider implements Provider { // The type will be determined dynamically, from the options passed in to the constructor type = ""; - urlPath = ""; + urlPath = "/contributor_statistics"; // Options can be set here and/or via the constructor _options: GitcoinGrantProviderOptions = { @@ -43,54 +42,40 @@ export class GitcoinGrantStatisticsProvider implements Provider { gitcoinGrantsStatistic; const errors = []; try { - const githubUser: GithubUserMetaData = await getGithubUserData(payload.proofs.code, context); - // Only check the contribution condition if a valid github id has been received - valid = !githubUser.errors && !!githubUser.id; - if (valid) { - const dataUrl = process.env.CGRANTS_API_URL + this.urlPath; - try { - gitcoinGrantsStatistic = await getGitcoinStatistics(dataUrl, githubUser.id, context); - } catch (error) { - errors.push(error); - } + const dataUrl = process.env.CGRANTS_API_URL + this.urlPath; + const address = payload.address.toLowerCase(); + gitcoinGrantsStatistic = await getGitcoinStatistics(dataUrl, address, context); - valid = - !gitcoinGrantsStatistic.error && - (gitcoinGrantsStatistic.record - ? gitcoinGrantsStatistic.record[this._options.receivingAttribute] >= this._options.threshold - : false); + valid = + !gitcoinGrantsStatistic.error && + (gitcoinGrantsStatistic.record + ? gitcoinGrantsStatistic.record[this._options.receivingAttribute] >= this._options.threshold + : false); - if (valid === true) { - record = { - // The type was previously incorrectly defined as string on the http response, - // and if we correctly called .toString() here instead of doing the forced cast, - // we would break our ability to hash against all previous records. - id: githubUser.id as unknown as string, - [this._options.recordAttribute]: `${this._options.threshold}`, - }; - } else { - errors.push( - `You do not qualify for this stamp. Your Grantee stats are less than the required thresholds: ${ - gitcoinGrantsStatistic.record[this._options.receivingAttribute] - } out of ${this._options.threshold}.` - ); - } - - if (!valid && errors.length === 0) { - errors.push(gitcoinGrantsStatistic.error); - } - return { - valid, - errors, - record, + if (valid === true) { + record = { + // The type was previously incorrectly defined as string on the http response, + // and if we correctly called .toString() here instead of doing the forced cast, + // we would break our ability to hash against all previous records. + address, + [this._options.recordAttribute]: `${this._options.threshold}`, }; } else { - const ret = { - valid: valid, - errors: githubUser ? githubUser.errors : undefined, - }; - return ret; + errors.push( + `You do not qualify for this stamp. Your Grantee stats are less than the required thresholds: ${ + gitcoinGrantsStatistic.record[this._options.receivingAttribute] + } out of ${this._options.threshold}.` + ); } + + if (!valid && errors.length === 0) { + errors.push(gitcoinGrantsStatistic.error); + } + return { + valid, + errors, + record, + }; } catch (e: unknown) { throw new ProviderExternalVerificationError(`Gitcoin Grants Statistic verification error: ${String(e)}.`); } @@ -103,18 +88,17 @@ type GitcoinStatisticsContext = { const getGitcoinStatistics = async ( dataUrl: string, - github_id: number, + address: string, context: GitcoinStatisticsContext ): Promise => { if (!context.gitcoinGrantStatistics?.[dataUrl]) { try { - // The gitcoin API expects lowercase handle - const grantStatisticsRequest = await axios.get(`${dataUrl}?github_id=${github_id}`, { + if (!context.gitcoinGrantStatistics) context.gitcoinGrantStatistics = {}; + + const grantStatisticsRequest = await axios.get(`${dataUrl}?address=${address}`, { headers: { Authorization: process.env.CGRANTS_API_TOKEN }, }); - if (!context.gitcoinGrantStatistics) context.gitcoinGrantStatistics = {}; - context.gitcoinGrantStatistics[dataUrl] = { record: grantStatisticsRequest.data } as GitcoinGrantStatistics; } catch (_error) { const error = _error as ProviderError; @@ -123,6 +107,11 @@ const getGitcoinStatistics = async ( error.response?.statusText } - Details: ${JSON.stringify(error?.response?.data)}`, }; + throw new ProviderExternalVerificationError( + `Error getting user info: ${error?.message} - Status ${error.response?.status}: ${ + error.response?.statusText + } - Details: ${JSON.stringify(error?.response?.data)}` + ); } } return context.gitcoinGrantStatistics[dataUrl]; diff --git a/platforms/src/GrantsStack/App-Bindings.ts b/platforms/src/GrantsStack/App-Bindings.ts deleted file mode 100644 index d38b002033..0000000000 --- a/platforms/src/GrantsStack/App-Bindings.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { AppContext, ProviderPayload } from "../types"; -import { Platform } from "../utils/platform"; - -export class GrantsStackPlatform extends Platform { - platformId = "GrantsStack"; - path = "GrantsStack"; - clientId: string = null; - redirectUri: string = null; - - banner = { - heading: `Note: Only Alpha and Beta rounds run by Gitcoin are included, and only - donations of $1 or larger are counted. For the Beta program, only matching-eligible - contributions are counted. Donations during Grants Round 18 (August '23) will be - added by the end of September.`, - }; - - async getProviderPayload(_appContext: AppContext): Promise { - const result = await Promise.resolve({}); - return result; - } -} diff --git a/platforms/src/GrantsStack/Providers-config.ts b/platforms/src/GrantsStack/Providers-config.ts deleted file mode 100644 index 4876988c98..0000000000 --- a/platforms/src/GrantsStack/Providers-config.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { PlatformSpec, PlatformGroupSpec, Provider } from "../types"; -import { GrantsStackProvider } from "./Providers/GrantsStack"; - -export const PlatformDetails: PlatformSpec = { - icon: "./assets/grantsStackLogo.svg", - platform: "GrantsStack", - name: "GrantsStack", - description: "Connect your existing GrantsStack Account to verify", - connectMessage: "Connect Account", -}; - -export const ProviderConfig: PlatformGroupSpec[] = [ - { - platformGroup: "Projects Contributed To:", - providers: [ - { title: "Supported 3+ unique projects", name: "GrantsStack3Projects" }, - { title: "Supported 5+ unique projects", name: "GrantsStack5Projects" }, - { title: "Supported 7+ unique projects", name: "GrantsStack7Projects" }, - ], - }, - { - platformGroup: "Matching Fund Programs Participation:", - providers: [ - { title: "Contributed to 2+ unique programs.", name: "GrantsStack2Programs" }, - { title: "Contributed to 4+ unique programs.", name: "GrantsStack4Programs" }, - { title: "Contributed to 6+ unique programs.", name: "GrantsStack6Programs" }, - ], - }, -]; - -export const providers: Provider[] = [ - new GrantsStackProvider({ - type: "GrantsStack3Projects", - dataKey: "projectCount", - threshold: 3, - }), - new GrantsStackProvider({ - type: "GrantsStack5Projects", - dataKey: "projectCount", - threshold: 5, - }), - new GrantsStackProvider({ - type: "GrantsStack7Projects", - dataKey: "projectCount", - threshold: 7, - }), - new GrantsStackProvider({ - type: "GrantsStack2Programs", - dataKey: "programCount", - threshold: 2, - }), - new GrantsStackProvider({ - type: "GrantsStack4Programs", - dataKey: "programCount", - threshold: 4, - }), - new GrantsStackProvider({ - type: "GrantsStack6Programs", - dataKey: "programCount", - threshold: 6, - }), -]; diff --git a/platforms/src/GrantsStack/Providers/GrantsStack.ts b/platforms/src/GrantsStack/Providers/GrantsStack.ts deleted file mode 100644 index 3c281cd198..0000000000 --- a/platforms/src/GrantsStack/Providers/GrantsStack.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { ProviderExternalVerificationError, type Provider, type ProviderOptions } from "../../types"; -import { ProviderContext, PROVIDER_ID, RequestPayload, VerifiedPayload } from "@gitcoin/passport-types"; -import axios from "axios"; - -// ----- Utils -import { handleProviderAxiosError } from "../../utils/handleProviderAxiosError"; - -export type GrantsStackProviderOptions = ProviderOptions & { - type: PROVIDER_ID; - dataKey: keyof GrantsStackCounts; - threshold: number; -}; - -type GrantsStackCounts = { - projectCount?: number; - programCount?: number; -}; - -export type GrantsStackContext = ProviderContext & { - grantsStack?: GrantsStackCounts; -}; - -type StatisticResponse = { - num_grants_contribute_to: number; - num_rounds_contribute_to: number; - total_valid_contribution_amount: number; - num_gr14_contributions: number; -}; - -export const getGrantsStackData = async ( - payload: RequestPayload, - context: GrantsStackContext -): Promise => { - try { - if (!context?.grantsStack?.projectCount || !context?.grantsStack?.programCount) { - const grantStatisticsRequest: { - data: StatisticResponse; - } = await axios.get(`${process.env.CGRANTS_API_URL}/allo/contributor_statistics`, { - headers: { Authorization: process.env.CGRANTS_API_TOKEN }, - params: { address: payload.address }, - }); - - if (!context.grantsStack) context.grantsStack = {}; - - context.grantsStack.projectCount = grantStatisticsRequest.data.num_grants_contribute_to; - context.grantsStack.programCount = grantStatisticsRequest.data.num_rounds_contribute_to; - - return context.grantsStack; - } - return context.grantsStack; - } catch (e) { - handleProviderAxiosError(e, "grant stack data", [payload.address]); - } -}; - -export class GrantsStackProvider implements Provider { - type: PROVIDER_ID; - threshold: number; - dataKey: keyof GrantsStackCounts; - - constructor(options: GrantsStackProviderOptions) { - this.type = options.type; - this.threshold = options.threshold; - this.dataKey = options.dataKey; - } - - async verify(payload: RequestPayload, context: ProviderContext): Promise { - try { - const errors = []; - let record = undefined; - const grantsStackData = await getGrantsStackData(payload, context); - const count = grantsStackData[this.dataKey]; - const valid = count >= this.threshold; - const contributionStatistic = `${this.type}-${this.threshold}-contribution-statistic`; - if (valid) { - record = { - address: payload.address, - contributionStatistic, - }; - } else { - errors.push(`You do not qualify for this stamp -- ${this.dataKey}: ${count} is less than ${this.threshold}`); - } - return { - valid, - record, - errors, - }; - } catch (error: unknown) { - throw new ProviderExternalVerificationError(`Grant Stack contribution verification error: ${String(error)}.`); - } - } -} diff --git a/platforms/src/GrantsStack/Providers/__tests__/GrantsStack.test.ts b/platforms/src/GrantsStack/Providers/__tests__/GrantsStack.test.ts deleted file mode 100644 index 2523374eba..0000000000 --- a/platforms/src/GrantsStack/Providers/__tests__/GrantsStack.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import axios from "axios"; -import { GrantsStackProvider, getGrantsStackData } from "../GrantsStack"; -import { RequestPayload } from "@gitcoin/passport-types"; - -// Mocking axios -jest.mock("axios"); - -// Common setup -const userAddress = "0x123"; -const requestPayload = { address: userAddress } as RequestPayload; - -describe("GrantsStackProvider", () => { - // Testing getGrantsStackData function - describe("getGrantsStackData", () => { - it("should fetch GrantsStack data successfully", async () => { - // Mock the axios response - (axios.get as jest.Mock).mockResolvedValue({ - data: { num_grants_contribute_to: 10, num_rounds_contribute_to: 5 }, - }); - const context = {}; - const result = await getGrantsStackData(requestPayload, context); - expect(result).toEqual({ projectCount: 10, programCount: 5 }); - }); - - it("should throw error when fetching GrantsStack data fails", async () => { - // Mock an axios error - (axios.get as jest.Mock).mockRejectedValue(new Error("Network Error")); - const context = {}; - await expect(() => getGrantsStackData(requestPayload, context)).rejects.toThrow("Network Error"); - }); - }); - - // Testing GrantsStackProvider class - describe("verify method", () => { - it("should verify GrantsStack data and return valid true if threshold is met", async () => { - const providerId = "GrantsStack5Projects"; - const threshold = 5; - const provider = new GrantsStackProvider({ type: providerId, threshold, dataKey: "projectCount" }); - // Using the previously tested getGrantsStackData, we'll assume it works as expected - const verifiedPayload = await provider.verify(requestPayload, { - grantsStack: { projectCount: 10, programCount: 1 }, - }); - expect(verifiedPayload).toMatchObject({ - valid: true, - record: { - address: userAddress, - contributionStatistic: `${providerId}-${threshold}-contribution-statistic`, - }, - }); - }); - - it("should verify GrantsStack data and return valid false if threshold is not met", async () => { - const projectCount = 10; - const programCount = 1; - const threshold = 15; - const providerId = "GrantsStack5Projects"; - const provider = new GrantsStackProvider({ type: providerId, threshold, dataKey: "projectCount" }); - const verifiedPayload = await provider.verify(requestPayload, { - grantsStack: { projectCount, programCount }, - }); - expect(verifiedPayload).toMatchObject({ - valid: false, - record: undefined, - errors: [`You do not qualify for this stamp -- projectCount: ${projectCount} is less than ${threshold}`], - }); - }); - - it("should throw an error if verification fails", async () => { - const providerId = "GrantsStack5Projects"; - // Mock the axios response to throw an error in getGrantsStackData - (axios.get as jest.Mock).mockRejectedValue(new Error("Network Error")); - const provider = new GrantsStackProvider({ type: providerId, threshold: 5, dataKey: "projectCount" }); - await expect(() => provider.verify(requestPayload, {})).rejects.toThrow( - "Grant Stack contribution verification error: Error: Network Error." - ); - }); - }); -}); diff --git a/platforms/src/GrantsStack/index.ts b/platforms/src/GrantsStack/index.ts deleted file mode 100644 index 1ff4029485..0000000000 --- a/platforms/src/GrantsStack/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { GrantsStackProvider } from "./Providers/GrantsStack"; -export { GrantsStackPlatform } from "./App-Bindings"; -export { ProviderConfig, PlatformDetails, providers } from "./Providers-config"; diff --git a/platforms/src/platforms.ts b/platforms/src/platforms.ts index 84fbc7bf61..2aa0b0f721 100644 --- a/platforms/src/platforms.ts +++ b/platforms/src/platforms.ts @@ -23,7 +23,6 @@ import * as Holonym from "./Holonym"; import * as Idena from "./Idena"; import * as Civic from "./Civic"; import * as CyberConnect from "./CyberProfile"; -import * as GrantsStack from "./GrantsStack"; import * as TrustaLabs from "./TrustaLabs"; import { PlatformSpec, PlatformGroupSpec, Provider } from "./types"; @@ -60,7 +59,6 @@ const platforms: Record = { Idena, Civic, CyberConnect, - GrantsStack, TrustaLabs, };