Skip to content

Commit

Permalink
feat(platforms): allo stamp (passportxyz#1579)
Browse files Browse the repository at this point in the history
* feat(platforms): base allo stamp

* feat(app, platforms, types): allo -> grantsstack stamp

* feat(app): allo stamp credential logic

* feat(app, platforms): add logo and banner for grants stack stamp

* chore(platforms): test grantsStack provider

* feat(platforms): use datakey to index allo count response

* chore(app): remove feature flag
  • Loading branch information
tim-schultz authored Aug 14, 2023
1 parent a5942d8 commit 0bb096c
Show file tree
Hide file tree
Showing 10 changed files with 261 additions and 3 deletions.
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

0 comments on commit 0bb096c

Please sign in to comment.