Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(platforms): allo stamp #1579

Merged
merged 7 commits into from
Aug 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app/context/ceramicContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const {
Idena,
Civic,
CyberConnect,
GrantsStack,
} = stampPlatforms;
import { PlatformProps } from "../components/GenericPlatform";

Expand Down Expand Up @@ -229,6 +230,11 @@ if (process.env.NEXT_PUBLIC_FF_CYBERCONNECT_STAMPS === "on") {
});
}

platforms.set("GrantsStack", {
platform: new GrantsStack.GrantsStackPlatform(),
platFormGroupSpec: GrantsStack.ProviderConfig,
});

export enum IsLoadingPassportState {
Idle,
Loading,
Expand Down
3 changes: 3 additions & 0 deletions app/public/assets/grantsStackLogo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions platforms/src/GrantsStack/App-Bindings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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. For the Alpha program, only donations larger than $1 are counted. For the Beta program, only matching-eligible contributions are counted.",
};

async getProviderPayload(appContext: AppContext): Promise<ProviderPayload> {
const result = await Promise.resolve({});
return result;
}

getOAuthUrl(state: string): Promise<string> {
throw new Error("Method not implemented.");
}
}
62 changes: 62 additions & 0 deletions platforms/src/GrantsStack/Providers-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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,
}),
];
83 changes: 83 additions & 0 deletions platforms/src/GrantsStack/Providers/GrantsStack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import type { Provider, ProviderOptions } from "../../types";
import { ProviderContext, PROVIDER_ID, RequestPayload, VerifiedPayload } from "@gitcoin/passport-types";
import axios from "axios";

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<GrantsStackCounts> => {
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) {
throw new Error("Error getting GrantsStack data");
}
};

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<VerifiedPayload> {
try {
const grantsStackData = await getGrantsStackData(payload, context);
const count = grantsStackData[this.dataKey];
const valid = count >= this.threshold;
const errors = !valid ? [`${this.dataKey}: ${count} is less than ${this.threshold}`] : [];
const contributionStatistic = `${this.type}-${this.threshold}-contribution-statistic`;
return {
valid,
errors,
record: {
address: payload.address,
contributionStatistic,
},
};
} catch (e) {
throw new Error("Error verifying GrantsStack data");
}
}
}
69 changes: 69 additions & 0 deletions platforms/src/GrantsStack/Providers/__tests__/GrantsStack.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import axios from "axios";
import { GrantsStackProvider, getGrantsStackData } from "../GrantsStack";
import { RequestPayload, PROVIDER_ID } 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("Error getting GrantsStack data");
});
});

// 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 providerId = "GrantsStack5Projects";
const provider = new GrantsStackProvider({ type: providerId, threshold: 15, dataKey: "projectCount" });
const verifiedPayload = await provider.verify(requestPayload, {
grantsStack: { projectCount: 10, programCount: 1 },
});
expect(verifiedPayload).toMatchObject({ valid: false });
});

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("Error verifying GrantsStack data");
});
});
});
3 changes: 3 additions & 0 deletions platforms/src/GrantsStack/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { GrantsStackProvider } from "./Providers/GrantsStack";
export { GrantsStackPlatform } from "./App-Bindings";
export { ProviderConfig, PlatformDetails, providers } from "./Providers-config";
2 changes: 2 additions & 0 deletions platforms/src/platforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ 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 { PlatformSpec, PlatformGroupSpec, Provider } from "./types";

type PlatformConfig = {
Expand Down Expand Up @@ -60,6 +61,7 @@ const platforms: Record<string, PlatformConfig> = {
Idena,
Civic,
CyberConnect,
GrantsStack,
};

if (process.env.NEXT_PUBLIC_FF_NEW_POAP_STAMPS === "on") {
Expand Down
2 changes: 1 addition & 1 deletion platforms/testSetup.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
process.env.ZKSYNC_ERA_MAINNET_ENDPOINT = "https://zksync-era-api-endpoint.io";
process.env.ZKSYNC_ERA_MAINNET_ENDPOINT = "https://zksync-era-api-endpoint.io";
11 changes: 9 additions & 2 deletions types/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,8 @@ export type PLATFORM_ID =
| "Holonym"
| "Idena"
| "Civic"
| "CyberConnect";
| "CyberConnect"
| "GrantsStack";

export type PROVIDER_ID =
| "Signer"
Expand Down Expand Up @@ -322,7 +323,13 @@ export type PROVIDER_ID =
| "twitterAccountAgeGte#730"
| "twitterTweetDaysGte#30"
| "twitterTweetDaysGte#60"
| "twitterTweetDaysGte#120";
| "twitterTweetDaysGte#120"
| "GrantsStack3Projects"
| "GrantsStack5Projects"
| "GrantsStack7Projects"
| "GrantsStack2Programs"
| "GrantsStack4Programs"
| "GrantsStack6Programs";

export type StampBit = {
bit: number;
Expand Down
Loading